Sealed Secrets: GitOps-Safe Secret Encryption for Kubernetes

Quick answer
A Kubernetes Secret is base64, not encryption — anyone with repo access can decode it. Sealed Secrets encrypts values asymmetrically before they ever touch git, so only the in-cluster controller can decrypt them. Here's how it works, how it differs from External Secrets Operator, and the key-backup mistake that locks teams out of their own secrets.
- How It Works
- Scope: Why You Can't Just Rename or Move One
- The Mistake That Locks You Out: Losing the Private Key
- Key Rotation
- Sealed Secrets vs. External Secrets Operator
7 min read · Security
A Kubernetes Secret is base64-encoded, not encrypted. Anyone with read access to the manifest — or to the git repo it's committed to, if you made that mistake — can decode it in one command: echo <value> | base64 -d. That's the whole reason teams either keep Secrets out of git entirely (and lose GitOps' single-source-of-truth guarantee) or reach for something that actually encrypts the value before it's committed.
Sealed Secrets (Bitnami, now maintained under the Kubernetes SIG) is one answer: asymmetric encryption client-side, so the encrypted blob is genuinely safe to commit, and only the in-cluster controller holding the private key can turn it back into a real Secret.
How It Works
The flow has three parts: a controller running in-cluster, a CLI (kubeseal) that runs wherever you write manifests, and a SealedSecret custom resource that replaces Secret in git.
kubectl create secret generic db-creds \
--from-literal=password=hunter2 \
--dry-run=client -o yaml \
| kubeseal --format yaml > db-creds-sealed.yaml
kubeseal fetches the controller's public certificate (over the cluster API, or from a cached .pem file for offline sealing) and encrypts each value in the Secret using it. The output is a SealedSecret — safe to commit, safe to read, safe to paste into a PR:
1apiVersion: bitnami.com/v1alpha1
2kind: SealedSecret
3metadata:
4 name: db-creds
5 namespace: production
6spec:
7 encryptedData:
8 password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq... # unreadable without the private key
9 template:
10 metadata:
11 name: db-creds
12 namespace: production
13 type: OpaqueApply the SealedSecret like any other manifest. The in-cluster controller watches for SealedSecret resources, decrypts them with its private key, and creates the corresponding real Secret in the same namespace — which your pods consume exactly like they would any other Secret, no application changes required.
Scope: Why You Can't Just Rename or Move One
By default, kubeseal binds the encrypted value to the exact namespace and name of the target Secret. Encrypt db-creds in production, and that ciphertext only decrypts into a Secret named db-creds in production — copy the YAML to staging or rename it, and the controller refuses to decrypt it.
This is a deliberate anti-replay protection, not a bug, but it trips people up during environment promotion. Three scope levels are available via --scope:
strict(default) — bound to namespace + name. Safest, most annoying for promotion pipelines.namespace-wide— bound to namespace only; the SealedSecret can be renamed freely within it.cluster-wide— decrypts in any namespace with any name. Use sparingly — it reintroduces some of the "any Secret with the ciphertext can be read anywhere" risk the tool exists to prevent.
For multi-environment GitOps (separate overlays per environment via Kustomize or Helm), most teams re-seal per environment rather than loosen scope — it's one extra kubeseal invocation per promotion, not a real bottleneck.
The Mistake That Locks You Out: Losing the Private Key
The controller generates its keypair on first startup and stores the private key as a regular Kubernetes Secret in its own namespace (kube-system by default). That Secret is not itself sealed — it's the root of trust, and it only exists in etcd.
If that namespace or the underlying etcd data is lost without a backup, every SealedSecret ever committed becomes permanently undecryptable — not "hard to recover," genuinely gone. The ciphertext is the same asymmetric encryption GitHub uses for SSH keys; there's no admin backdoor.
Back it up explicitly and immediately after install:
kubeseal --fetch-cert > sealed-secrets-public-cert.pem # public cert — fine to commit, used for offline sealing
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > sealed-secrets-private-keys-backup.yaml # PRIVATE — store outside git, in a vault or offlineRestore it onto a fresh cluster by applying that backup before the controller's first startup, and it'll pick up the existing keypair instead of generating a new one — every previously-sealed secret decrypts immediately.
Server & SSH Hardening Checklist
The firewall, SSH, fail2ban, and update baseline every internet-facing Linux box should pass. Plain Markdown you can run down in an afternoon.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
Key Rotation
The controller rotates its active signing key automatically (30-day default, configurable via --rotate-period), but it retains old private keys so previously-sealed secrets keep decrypting. Rotation doesn't invalidate anything already committed — it only changes which key kubeseal encrypts new secrets against. Re-encrypt against the latest key only if you're deliberately retiring an old one (e.g., after a suspected compromise), using kubeseal --re-encrypt.
Sealed Secrets vs. External Secrets Operator
Both solve "keep real credential values out of git," but from opposite directions, and the choice matters:
| Sealed Secrets | External Secrets Operator | |
|---|---|---|
| Source of truth | The encrypted value lives in git | An external store (AWS Secrets Manager, Vault, etc.) is the source of truth |
| Rotation | Re-seal and commit a new value | Rotate in the external store; ESO syncs automatically |
| Offline / air-gapped | Works fully offline — no runtime dependency on an external service | Requires network access to the secret store at sync time |
| External infra required | None — self-contained in-cluster | Yes — a secrets manager or Vault cluster |
| Best fit | Teams that want secrets fully inside Git/Kubernetes, no external system | Teams already standardized on a cloud secrets manager, wanting rotation without a redeploy |
If you're already running External Secrets Operator against AWS Secrets Manager or Vault, adding Sealed Secrets alongside it is usually redundant — pick one model per cluster rather than mixing both for the same class of secret.
Frequently Asked Questions
Is a SealedSecret safe to commit to a public repository?
Yes, for the ciphertext itself — it's encrypted with the controller's public key, and without the matching private key (which never leaves the cluster) it cannot be decrypted. The usual caveats still apply: don't commit the private key backup, and remember that anyone who can create a SealedSecret resource in your cluster's namespaces can trigger decryption by applying it — RBAC on SealedSecret/Secret creation still matters.
Can I decrypt a SealedSecret without the cluster?
No, by design. kubeseal only encrypts (using the public cert); decryption only happens inside the controller, which is the entire point. If you need to inspect a value, kubectl get secret <name> -o jsonpath='{.data.password}' | base64 -d against the real Secret the controller created — not the SealedSecret.
What happens if I apply a SealedSecret before the controller is running?
Nothing decrypts it — the SealedSecret resource just sits there until the controller starts, notices it, and creates the real Secret. This is normal during a fresh cluster bootstrap via GitOps (Argo CD/Flux); order the controller's own install to sync first if your tooling doesn't already retry.
Does this replace RBAC on Secrets?
No. Sealed Secrets protects the value in git and in transit to the cluster. Once the controller decrypts it into a real Secret, standard Kubernetes RBAC governs who and what can read it. Sealing a secret and then granting broad get secrets access in that namespace defeats the purpose.
For the runtime-pull alternative to this git-committed model, see External Secrets Operator on Kubernetes. For the broader configuration-management patterns Secrets fit into, see Kubernetes ConfigMaps and Secrets: Configuration Management Patterns.
Setting up a GitOps pipeline that needs real secret management, not just YAML in a private repo? Talk to us at Coding Protocols — we help platform teams design secret-handling that survives an audit.
Official References
- Sealed Secrets (Kubernetes SIG) — controller source, kubeseal CLI, scope and rotation flags
- Kubernetes Secrets documentation — base encoding behavior and built-in encryption-at-rest options
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


