Sealed Secrets
Encrypt Kubernetes Secrets so they can be safely stored in Git using Bitnami Sealed Secrets.
Time: ~15 minutes Difficulty: Intermediate
What You Will Learn
Section titled “What You Will Learn”- Why base64 encoding is not encryption
- Installing the Sealed Secrets controller with Helm
- Using kubeseal to encrypt Secrets with asymmetric cryptography
- How the controller decrypts SealedSecrets into regular Secrets
- Scoping sealed secrets to namespace vs cluster-wide
- Rotating encryption keys safely
Prerequisites
Section titled “Prerequisites”Install the kubeseal CLI:
# macOSbrew install kubeseal
# Linuxwget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/kubeseal-0.24.0-linux-amd64.tar.gztar xfz kubeseal-0.24.0-linux-amd64.tar.gzsudo install -m 755 kubeseal /usr/local/bin/kubeseal
# Verifykubeseal --versionWhy Sealed Secrets
Section titled “Why Sealed Secrets”Regular Kubernetes Secrets are base64 encoded, not encrypted. Anyone with read access to the namespace can decode them. This means you cannot safely commit Secrets to Git, which breaks GitOps workflows.
Sealed Secrets solve this problem:
- The controller generates a public/private key pair
- You encrypt Secrets with the public key using kubeseal
- Only the controller can decrypt them (private key never leaves the cluster)
- Encrypted SealedSecrets are safe to commit to Git
- The controller automatically decrypts them into regular Secrets
Install Sealed Secrets Controller
Section titled “Install Sealed Secrets Controller”Navigate to the demo directory:
cd demos/sealed-secretskubectl apply -f manifests/namespace.yaml
# Install the controller in the sealed-secrets namespacehelm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secretshelm repo update
helm install sealed-secrets sealed-secrets/sealed-secrets \ --namespace sealed-secrets \ --create-namespace \ --set-string fullnameOverride=sealed-secretsWait for the controller to be ready:
kubectl get pods -n sealed-secrets -wDeploy
Section titled “Deploy”Fetch the Public Key
Section titled “Fetch the Public Key”The controller generates a public key for encryption. Fetch it:
kubeseal --fetch-cert \ --controller-namespace sealed-secrets \ --controller-name sealed-secrets > /tmp/sealed-secrets-pub.pem
cat /tmp/sealed-secrets-pub.pemThis public key can be safely shared. Anyone can encrypt Secrets with it, but only the controller can decrypt.
Seal a Secret
Section titled “Seal a Secret”Create a regular Secret and encrypt it:
# Create a Secret (dry-run, don't apply)kubectl create secret generic db-credentials \ --namespace sealed-secrets-demo \ --from-literal=DB_USER=appuser \ --from-literal=DB_PASSWORD=s3cret-passw0rd \ --from-literal=DB_HOST=postgres.sealed-secrets-demo.svc \ --dry-run=client -o yaml > /tmp/secret.yaml
# Seal it with kubesealkubeseal --controller-namespace sealed-secrets \ --controller-name sealed-secrets \ --format yaml < /tmp/secret.yaml > /tmp/sealed-secret.yaml
cat /tmp/sealed-secret.yamlThe output shows a SealedSecret with encrypted data. Apply it:
kubectl apply -f /tmp/sealed-secret.yamlDeploy the Application
Section titled “Deploy the Application”kubectl apply -f manifests/deployment.yamlkubectl apply -f manifests/service.yamlVerify
Section titled “Verify”Check that the SealedSecret was decrypted into a regular Secret:
# The SealedSecret resourcekubectl get sealedsecrets -n sealed-secrets-demo
# The controller created a regular Secretkubectl get secrets -n sealed-secrets-demokubectl describe secret db-credentials -n sealed-secrets-demoVerify the pods can read the secret values:
kubectl exec -n sealed-secrets-demo deploy/demo-app -- env | grep DB_You should see the database credentials as environment variables.
Inspect the Encrypted Data
Section titled “Inspect the Encrypted Data”Compare the SealedSecret to the original Secret:
# Original Secret (base64, easily decoded)cat /tmp/secret.yaml
# SealedSecret (encrypted, safe for Git)cat /tmp/sealed-secret.yamlTry decoding the base64 Secret:
echo "czNjcmV0LXBhc3N3MHJk" | base64 -d# s3cret-passw0rd (easily decoded!)The SealedSecret encrypted data cannot be decrypted without the controller’s private key.
What is Happening
Section titled “What is Happening”manifests/ namespace.yaml # sealed-secrets-demo namespace secret-original.yaml # Example regular Secret (DO NOT commit to Git!) sealed-secret.yaml # Placeholder SealedSecret (safe to commit) deployment.yaml # nginx Deployment reading secret as env vars service.yaml # ClusterIP Service
Installed via Helm: sealed-secrets-controller # Runs in sealed-secrets namespaceEncryption flow:
1. Controller generates RSA key pair on startup2. Public key is available via kubeseal --fetch-cert3. User creates a Secret (dry-run, not applied)4. kubeseal encrypts the Secret with the public key5. User commits SealedSecret to Git (safe, encrypted)6. User applies SealedSecret to the cluster7. Controller decrypts it with the private key8. Controller creates a regular Secret9. Pods consume the Secret normallyScope:
By default, SealedSecrets are scoped to the namespace and name specified in metadata. This means:
- The encrypted data is bound to the namespace and Secret name
- You cannot copy a SealedSecret to a different namespace or rename it
- This prevents attacks where someone moves your SealedSecret elsewhere
You can change the scope with kubeseal flags:
--scope strict(default): namespace + name binding--scope namespace-wide: can rename, but locked to namespace--scope cluster-wide: can use anywhere (least secure)
Experiment
Section titled “Experiment”-
Rotate the secret and re-seal it:
Terminal window kubectl create secret generic db-credentials \--namespace sealed-secrets-demo \--from-literal=DB_USER=appuser \--from-literal=DB_PASSWORD=new-passw0rd-123 \--from-literal=DB_HOST=postgres.sealed-secrets-demo.svc \--dry-run=client -o yaml \| kubeseal --controller-namespace sealed-secrets \--controller-name sealed-secrets \--format yaml | kubectl apply -f -# Restart the deployment to pick up the new secretkubectl rollout restart deploy/demo-app -n sealed-secrets-demokubectl rollout status deploy/demo-app -n sealed-secrets-demo# Verifykubectl exec -n sealed-secrets-demo deploy/demo-app -- env | grep DB_PASSWORD -
Try to decrypt the SealedSecret without the controller (should fail):
Terminal window # Fetch the encrypted valuekubectl get sealedsecret db-credentials -n sealed-secrets-demo -o yaml# No way to decrypt it without the controller's private key -
Test namespace scoping by trying to copy the SealedSecret to another namespace:
Terminal window kubectl create namespace test-copykubectl get sealedsecret db-credentials -n sealed-secrets-demo -o yaml | \sed 's/sealed-secrets-demo/test-copy/g' | kubectl apply -f -# The controller will fail to decrypt it because the scope is bound to sealed-secrets-demokubectl get events -n test-copy --sort-by='.lastTimestamp'kubectl delete namespace test-copy -
Check the controller logs to see decryption events:
Terminal window kubectl logs -n sealed-secrets -l app.kubernetes.io/name=sealed-secrets -
Create a cluster-wide scoped SealedSecret:
Terminal window kubectl create secret generic api-key \--namespace sealed-secrets-demo \--from-literal=KEY=sk-live-abc123def456 \--dry-run=client -o yaml \| kubeseal --controller-namespace sealed-secrets \--controller-name sealed-secrets \--scope cluster-wide \--format yaml | kubectl apply -f -# This can be renamed or moved to another namespacekubectl get secret api-key -n sealed-secrets-demo
Cleanup
Section titled “Cleanup”# Delete the demo namespacekubectl delete namespace sealed-secrets-demo
# Uninstall the controllerhelm uninstall sealed-secrets -n sealed-secretskubectl delete namespace sealed-secrets
# Clean up temp filesrm -f /tmp/secret.yaml /tmp/sealed-secret.yaml /tmp/sealed-secrets-pub.pemFurther Reading
Section titled “Further Reading”See docs/deep-dive.md for a detailed explanation of Sealed Secrets architecture, key rotation strategies, backup and recovery, offline encryption workflows, and comparison with other secret management solutions like Vault and External Secrets Operator.
Next Step
Section titled “Next Step”Move on to Vertical Pod Autoscaler to learn how VPA automatically right-sizes container resource requests based on actual usage.