SOPS and Sealed Secrets let you commit encrypted secrets to Git, but the ciphertext lives forever in repository history, so the whole design rests on how well the decryption keys are scoped, backed up and replaced. Both tools answer the same GitOps question: how do you keep a declarative repository complete without putting plaintext credentials in it? They answer it differently. SOPS encrypts values inside YAML, JSON, ENV, INI or binary files against KMS, age or PGP recipients; Sealed Secrets encrypts a whole Kubernetes Secret to a public key whose private half only an in-cluster controller holds. This guide compares the two, explains the cryptography and scoping that matter, sets out key management and compromise response, and gives a rollout plan and checks. It is for platform engineers running Flux or Argo CD. The GitOps Security Guide gives the overview; this page goes deeper on these two tools.
Key takeaways
- Git history is permanent. Anyone who later obtains a decryption key can read every old revision, so a leaked key means rotating the real credentials, not just re-encrypting files.
- Sealed Secrets binds ciphertext to a name and namespace. The default
strictscope uses the namespace and name as the RSA-OAEP label;cluster-wideremoves that binding and lets a sealed value be unsealed anywhere. - SOPS leaves structure readable. Key names and the
sopsmetadata stay in cleartext for diffing; only matched values are encrypted, so setencrypted_regexdeliberately. - Key renewal is not secret rotation. The Sealed Secrets project says renewal and re-encryption are not a substitute for rotating the secret values; SOPS's
rotatechanges the data key, not your database password. - Prefer cloud KMS identities to key files. A SOPS rule using KMS ties decryption to an IAM role you can audit and revoke; an age or PGP private key on a laptop cannot be audited at all.
How the two tools differ
| Aspect | SOPS | Sealed Secrets |
|---|---|---|
| What is encrypted | Leaf values in YAML, JSON, ENV, INI; whole file in BINARY mode | A whole Kubernetes Secret, as a SealedSecret custom resource |
| Cryptography | Each value AES256_GCM with a random 256-bit data key; MAC over all values; data key encrypted to each recipient | Single-use AES-256-GCM session key, wrapped with RSA-OAEP (SHA-256) using a 4096-bit controller key by default |
| Who can decrypt | Anyone holding a recipient key: KMS (AWS, GCP, Azure, HuaweiCloud), HashiCorp Vault transit, age or PGP | Only the controller holding the private sealing keys |
| Where decryption happens | On a laptop, in CI, or in the cluster (Flux kustomize-controller with decryption.provider: sops) | In the cluster only |
| Readable in review | Key names and structure; values encrypted | Metadata only; data is an opaque blob |
| Works outside Kubernetes | Yes: Terraform variables, application config, Helm values | No |
| Main key to protect | KMS key policy or the age/PGP private key | Sealing key Secrets labelled sealedsecrets.bitnami.com/sealed-secrets-key |
- Choose Sealed Secrets when: secrets are only Kubernetes Secrets, developers should be able to encrypt but never decrypt, and you do not want a cloud KMS dependency.
- Choose SOPS when: the same encrypted file feeds several tools or clusters, reviewers need to see which keys changed, or you want decryption governed by cloud IAM.
- Choose neither when: you already run a central secrets manager and need frequent rotation. Referencing values with the External Secrets Operator security guide approach keeps ciphertext out of Git altogether.
Sealed Secrets: scopes, sealing keys and trust
| Scope | How to request it | OAEP label | Risk |
|---|---|---|---|
strict (default) | Nothing, or kubeseal --scope strict | Namespace and name | Lowest: a copied blob fails under any other name or namespace |
namespace-wide | --scope namespace-wide or annotation sealedsecrets.bitnami.com/namespace-wide: "true" | Namespace | Anyone who can create Secrets in that namespace can rename the value into another workload |
cluster-wide | --scope cluster-wide or annotation sealedsecrets.bitnami.com/cluster-wide: "true" (wins if both set) | Empty | Highest: anyone with the blob can unseal it into a namespace they control |
- Sealing authenticates no one: the project notes that anyone can create a SealedSecret for a given namespace and name. What reaches the cluster must be controlled by repository review and Kubernetes RBAC on
sealedsecretsand Secrets. - Key renewal: the controller generates a new sealing key every 30 days by default (
--key-renew-period;0disables it). Old keys are kept so existing SealedSecrets still decrypt, and they are not garbage collected. - Back up every key: the documented backup is
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml. That file contains private keys: store it in a vault or HSM-backed store, refresh it after each renewal, and restrict who can read it. - Use the right certificate:
kubeseal --fetch-certretrieves the public certificate for offline sealing with--cert. Distribute it from a trusted location so a developer is not tricked into sealing to an attacker's key. - Validate before merge:
kubeseal --validatein CI catches blobs sealed to the wrong cluster or scope.
SOPS: creation rules, recipients and integrity
SOPS behaviour is driven by .sops.yaml creation_rules. Review this file as carefully as code, because it decides who can decrypt every new file.
| Setting | Purpose | Security guidance |
|---|---|---|
path_regex | Selects files a rule applies to | Separate rules for each environment, so production files are encrypted only to production keys |
kms, gcp_kms, azure_keyvault, hc_vault_transit_uri, age, pgp | Recipients of the data key | Prefer KMS; use the AWS role and context fields so decryption needs a specific role and encryption context |
encrypted_regex (or suffix and comment variants) | Which values are encrypted | For Kubernetes Secrets use ^(data|stringData)$, as the Flux guide recommends; never leave secrets in cleartext keys |
key_groups with shamir_threshold | Split the data key so several groups must cooperate | Use for break-glass or highly sensitive files (for example threshold 2 of KMS, offline age, offline PGP) |
mac_only_encrypted | When true, only encrypted values feed the MAC | Keep the default false so tampering with cleartext values is also detected |
- Integrity: SOPS computes a MAC over all values and stores it encrypted under
sops.mac, which is why editing a file needs the private key, not just the public one. - Recipient changes:
sops updatekeysadds or removes recipients from.sops.yamlwithout changing the data key;sops rotategenerates a new data key and re-encrypts every value. After removing a recipient, rotate: the removed holder may already have the old data key. - Key strength: SOPS does not enforce PGP key size. Its security notes recommend at least 2048-bit RSA or 256-bit ECDSA and keeping private keys on smart cards or offline.
- Flux decryption: set
decryption.provider: sopson the Kustomization with asecretRef(for age the key entry must end.agekey), or bind an IAM role with KMS decrypt to the kustomize-controller service account influx-systemand hold no key file at all.
Where identity fits: the decryption key is a machine credential
The sealing key, the age identity and the KMS role are non-human identities with read access to every secret in the repository's history. Give each an owner, a scope (one per cluster or environment, never one key for all), a backup and a revocation plan. A KMS key used by SOPS should allow kms:Decrypt only to the in-cluster controller role and to a small break-glass group, with encryption allowed more widely. See the Secrets Management Guide and the Secrets Zero Problem Guide.
Key compromise and rotation
Assume any secret encrypted to a leaked key is exposed, including every historic revision. The two tools need different steps.
- Scope the exposure: list every file or SealedSecret encrypted to the key across all branches and history.
- Sealed Secrets: force a new sealing key early with
--key-cutoff-time(RFC1123 time, as produced bydate -R), mark the leaked key, rotate the real credentials, reseal them with the new certificate, then retire the old key once nothing depends on it. - SOPS: follow the documented order: remove the key from
.sops.yaml, runsops updatekeys, runsops rotate, and only then rotate the underlying passwords and API keys. Doing it out of order can leave the compromised key with access. - Verify: confirm workloads use the new credentials and the old ones fail. See the Secrets Rotation Runbook Template.
Implementation plan
- Find plaintext first: scan repositories, including history, for Kubernetes Secret manifests; scanners must decode base64. The exposed Kubernetes Secrets case involving SAP found registry credentials in committed Secret files that had been public for years.
- Pick per use case: Sealed Secrets for cluster-only secrets with no KMS, SOPS with KMS where files cross tools or environments.
- Pilot one non-production cluster: install the controller or Flux decryption, encrypt a test secret, confirm decryption and
kubeseal --validateor asopsdecrypt check in CI. - Add guardrails: pre-commit and CI checks that reject unencrypted Secret kinds,
cluster-wideannotations without approval, and.sops.yamlchanges without platform review (CODEOWNERS). - Migrate and rotate: move each secret, rotate the value (the old one was in plaintext), and remove plaintext from history where feasible.
- Rollback: keep the previous controller version and backed-up sealing keys; restoring the keys restores decryption of existing blobs. For SOPS, older files remain decryptable as long as their recipients exist.
Detection and measurement
- Repository signals: commits adding
kind: Secretoutside approved paths, edits to.sops.yaml, and new scope annotations. See the Git Secret Scanning Guide. - Cluster signals: Kubernetes audit events for reads of sealing key Secrets or the
sops-ageSecret, and SealedSecret unseal errors in controller logs. - KMS signals: CloudTrail
Decryptcalls on the SOPS key from any principal other than the controller role or break-glass users.
| Metric | Target |
|---|---|
| Plaintext Secret manifests found in repositories (including history) | 0 new per month |
SealedSecrets using cluster-wide scope without an exception record | 0 |
| Days since the last verified sealing key backup restore test | 90 or fewer |
| SOPS files encrypted to personal age or PGP keys rather than KMS or shared break-glass keys | Trending to 0 |
Days since last sops rotate on production files | Within your rotation policy |
Framework mapping
| Practice | NIST CSF 2.0 | NIST SP 800-53 r5 | MITRE ATT&CK |
|---|---|---|---|
| Encrypt secrets before they reach Git | PR.DS-01 | SC-28, SC-13 | T1552.001 Credentials In Files, T1213.003 Code Repositories |
| Manage sealing keys and KMS keys | PR.AA-01 | SC-12, IA-5 | T1552.004 Private Keys |
| Back up and test restore of sealing keys | PR.DS-11 | CP-9 | None specific |
Review changes to .sops.yaml and scopes | PR.PS-01 | CM-3 | None specific |
| Rotate after key compromise | RS.MI-02 | IA-5 | None specific |
Common failure patterns
- One age key for every environment: a single developer laptop compromise decrypts production and every past revision.
- Sealing keys lost with the cluster: no backup after renewal, so a rebuilt cluster cannot decrypt anything in Git.
- Re-encrypting instead of rotating:
kubeseal --re-encryptorsops rotateafter a leak, while the actual credentials stay unchanged. - Cluster-wide scope by default: chosen to make copying easy, it removes the protection that binds a value to its workload.
- Loose
encrypted_regex: a rule that missesstringDataor a config key commits secrets in cleartext beside encrypted ones.
Practitioner checklist
- Keep the Sealed Secrets
strictscope unless an approved exception exists. - Back up sealing keys after every renewal and test a restore at least quarterly.
- Use a separate sealing key or SOPS recipient set per cluster or environment.
- Prefer KMS recipients with role and encryption context over age or PGP key files.
- Set
encrypted_regex: ^(data|stringData)$for Kubernetes Secrets and keepmac_only_encryptedfalse. - Protect
.sops.yamland controller configuration with CODEOWNERS review. - Run
kubeseal --validateor a decrypt check in CI, and block plaintext Secret kinds. - On key compromise, follow the documented order and rotate the real credentials.
Standards and references
- SOPS: Reference (.sops.yaml and encryption)
- SOPS: Key management
- SOPS: Security
- SOPS: GitHub repository
- Bitnami Labs: Sealed Secrets README
- Bitnami Labs: Sealed Secrets cryptography
- Flux: Manage Kubernetes secrets with SOPS
Related NHI Mgmt Group resources: GitOps Security Guide · Kubernetes Secrets Security Guide · External Secrets Operator security guide · Git Secret Scanning Guide