GitOps wants the whole desired state in the repository. A Kubernetes Secret is not encrypted — it is base64, which is encoding, not protection — so committing one to Git leaks the credential to anyone with read access to the repo.
Bitnami Sealed Secrets closes that gap without pulling in an external vault. You run a controller in the cluster that holds a private key. You encrypt a Secret against the matching public certificate with the kubeseal CLI, producing a SealedSecret custom resource that only that controller can decrypt. The SealedSecret is safe to commit, safe to push, safe to sit in a public repository. The controller watches for it, decrypts it in-cluster, and reconciles a normal Secret that your pods consume as usual. This walks the full loop: install, seal, commit, back up the key, rotate, and how it fits Argo CD or Flux.
Install the controller and the CLI
The controller ships as a single release manifest. Apply it, then install a matching kubeseal binary. Keep the CLI and controller versions aligned; check the releases page for the current version rather than copying a pin blindly.
Free · 4 minutes
Would you survive contact with a determined attacker — or an auditor?
Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.
# Install the controller into the kube-system namespace
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.1/controller.yaml
# Install the kubeseal CLI (Linux amd64 example)
KUBESEAL_VERSION=0.27.1
curl -sSfL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz" \
| tar -xz kubeseal
sudo install -m 0755 kubeseal /usr/local/bin/kubeseal
# Confirm the controller is running
kubectl get pods -n kube-system -l name=sealed-secrets-controller
On its first start the controller generates an RSA key pair and stores it as a Secret in its own namespace, labelled sealedsecrets.bitnami.com/sealed-secrets-key=active. That private key is the only thing that can decrypt anything you seal. Treat it as the crown jewel — we come back to backing it up below.
Seal a secret and commit it
The pattern is: build a normal Secret manifest locally, pipe it through kubeseal, and commit only the sealed output. Never commit the plaintext. Use --dry-run so the raw Secret is never applied to the cluster or written to disk.
# Generate the plaintext Secret in memory, seal it, write only the SealedSecret
kubectl create secret generic db-credentials \
--namespace=payments \
--from-literal=DATABASE_URL='postgres://app:s3cr3t@db:5432/ledger' \
--dry-run=client -o yaml \
| kubeseal --format yaml > sealedsecret-db-credentials.yaml
git add sealedsecret-db-credentials.yaml
git commit -m 'Add sealed DB credentials for payments'
By default kubeseal reaches the controller through the Kubernetes API to fetch the public certificate, so it needs cluster access at seal time. For CI runners or an air-gapped laptop, fetch the certificate once and seal offline against the file:
# Fetch and store the public cert once
kubeseal --fetch-cert > pub-cert.pem
# Seal offline — no cluster contact needed
kubectl create secret generic db-credentials --namespace=payments \
--from-literal=DATABASE_URL='postgres://app:s3cr3t@db:5432/ledger' \
--dry-run=client -o yaml \
| kubeseal --cert pub-cert.pem --format yaml > sealedsecret-db-credentials.yaml
The public certificate is exactly that — public. Publishing it, or checking it into the repo for CI to use, gives away nothing.
Understand scope before you rename anything
This is where teams get surprised. By default sealing is strict: the secret’s name and namespace are folded into the encrypted data. A SealedSecret sealed as db-credentials in payments will refuse to decrypt if you rename it or move it to another namespace. That is a security feature — it stops someone copying your sealed blob into a namespace they control and unsealing it there.
Two looser scopes exist when you genuinely need them. Namespace-wide lets you rename the sealed secret freely within one namespace. Cluster-wide lets it be unsealed under any name in any namespace. Set the scope with a flag or an annotation on the input Secret:
# Flag form
kubeseal --scope namespace-wide --format yaml < secret.yaml > sealed.yaml
# Annotation form on the input Secret
# sealedsecrets.bitnami.com/namespace-wide: "true"
# sealedsecrets.bitnami.com/cluster-wide: "true"
Reach for the looser scopes deliberately, not to make an error message go away. Strict is the right default for almost everything. The discipline here is the same one behind good API key scoping and rotation: bind a credential as narrowly as the workload allows.
Back up the private key, or lose everything
If the controller’s namespace is wiped and you have no backup, every SealedSecret in Git becomes undecryptable ciphertext. Back the key up immediately after install and store it somewhere out of band — an offline vault, a break-glass store — not the same repo.
# Export the controller's public + private keys
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > sealed-secrets-key-backup.yaml
# Restore into a fresh cluster, then restart the controller to pick it up
kubectl apply -f sealed-secrets-key-backup.yaml
kubectl delete pod -n kube-system -l name=sealed-secrets-controller
That backup file contains the private key. It is as sensitive as every secret it protects combined — encrypt it at rest and control access tightly.
Rotation and re-encryption
The controller renews its sealing key every 30 days by default, controlled by --key-renew-period (a value of 0 disables renewal). Renewal generates a new active key; old keys are retained so existing SealedSecret resources still decrypt. Nothing you have committed breaks on renewal.
Before you decommission an old key, re-encrypt your sealed secrets against the current one:
kubeseal --re-encrypt < sealedsecret-db-credentials.yaml > tmp.yaml \
&& mv tmp.yaml sealedsecret-db-credentials.yaml
One honest caveat: re-encryption does not rotate the underlying credential. The old ciphertext still sits in your Git history, and anyone who ever held the private key could have read it. If a secret is actually compromised, rotate the real database password or API key at source and seal the new value — do not rely on re-encryption to undo exposure.
How it fits Argo CD and Flux
Nothing special is required. A SealedSecret is an ordinary custom resource, so Argo CD or Flux syncs it from the repo like any other manifest. The controller sets an owner reference from the SealedSecret to the Secret it produces, which means your GitOps tool manages the sealed resource and leaves the derived plaintext Secret alone — no fights over drift. The common snag is ordering: the controller and its CRD must be installed before the first SealedSecret syncs, so make it an early sync-wave or a separate bootstrap application.
Back this with a pre-commit baseline that blocks plaintext secrets so an unsealed manifest can never reach the branch in the first place. Together they give you the property GitOps actually promises — the repo is the source of truth, and it is safe to read — without slowing the deployment flow you are measuring.
Sealed Secrets is not a full secrets-management platform — no dynamic leases, no fine-grained audit of reads. What it is, is the smallest honest way to put encrypted credentials in Git. For most Kubernetes teams doing GitOps, that is precisely the gap that needed closing.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Most technology problems are not technology problems. They are control problems.
The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.