Security
7 min readSeptember 21, 2026

Sealed Secrets: GitOps-Safe Secret Encryption for Kubernetes

AJ
Ajeet Yadav
Platform & Cloud Engineer
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.

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:

yaml
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: Opaque

Apply 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:

bash
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 offline

Restore 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 SecretsExternal Secrets Operator
Source of truthThe encrypted value lives in gitAn external store (AWS Secrets Manager, Vault, etc.) is the source of truth
RotationRe-seal and commit a new valueRotate in the external store; ESO syncs automatically
Offline / air-gappedWorks fully offline — no runtime dependency on an external serviceRequires network access to the secret store at sync time
External infra requiredNone — self-contained in-clusterYes — a secrets manager or Vault cluster
Best fitTeams that want secrets fully inside Git/Kubernetes, no external systemTeams 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

Was this article helpful?

Be the first to rate this article

Related Topics

Kubernetes
Sealed Secrets
GitOps
Secrets Management
Security
Platform Engineering
Bitnami

Found this useful? Share it.

Practice this

Related tools

Read Next