Overview
Sealed Secrets provides a secure way to store Kubernetes secrets in version control. It encrypts your secrets into SealedSecret resources that can only be decrypted by the controller running in your cluster.
Why Sealed Secrets? Regular Kubernetes Secrets are base64-encoded, not encrypted. Sealed Secrets allows you to safely commit encrypted secrets to Git while maintaining GitOps workflows.
How It Works
Controller : Runs in your cluster with a private key
Public Certificate : Used by kubeseal CLI to encrypt secrets
Encryption : Your secrets are encrypted locally using the public key
Storage : Encrypted SealedSecrets can be safely stored in Git
Decryption : Controller decrypts SealedSecrets into regular Secrets automatically
Installation
Step 1: Install Sealed Secrets Controller
Install using Helm:
# Add the Sealed Secrets Helm repository
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
# Install the controller in kube-system namespace
helm install sealed-secrets -n kube-system \
--set-string fullnameOverride=sealed-secrets-controller \
sealed-secrets/sealed-secrets
Step 2: Install kubeseal CLI
Install the client-side tool for creating sealed secrets:
# Set the version (check for latest at https://github.com/bitnami-labs/sealed-secrets/releases)
KUBESEAL_VERSION = '0.29.0'
# Download and extract
curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${ KUBESEAL_VERSION }/kubeseal-${ KUBESEAL_VERSION }-linux-amd64.tar.gz"
tar -xvzf kubeseal- ${ KUBESEAL_VERSION } -linux-amd64.tar.gz kubeseal
# Install
sudo install -m 755 kubeseal /usr/local/bin/kubeseal
Step 3: Verify Installation
Check that the controller is running and fetch the public certificate:
# Check controller status
kubectl get pods -n kube-system -l app.kubernetes.io/name=sealed-secrets
# Fetch the public certificate (confirms controller is ready)
kubeseal --fetch-cert
Creating Sealed Secrets
Method 1: From kubectl (Recommended)
Create a sealed secret directly from kubectl without creating an intermediate file:
# Create sealed secret from literal values
kubectl create secret generic exchange-router-secret \
--dry-run=client \
--from-literal=DATABASE_URL= "postgresql://user:pass@host:5432/db" \
--from-literal=API_KEY= "your-api-key" \
-o yaml | \
kubeseal \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
--format yaml > sealed-secret.yml
Method 2: From Secret File
Create a regular secret file first, then seal it:
# sample-secret.yml
apiVersion : v1
kind : Secret
metadata :
name : exchange-router-secret
namespace : default
type : Opaque
stringData :
DATABASE_URL : "postgresql://user:pass@host:5432/db"
API_KEY : "your-api-key"
Then seal it:
kubeseal --format yaml < sample-secret.yml > sealed-secret.yml
Method 3: Offline Certificate
For CI/CD pipelines where you don’t have cluster access:
# Fetch and save the certificate once
kubeseal \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
--fetch-cert > mycert.pem
Then use the local certificate for sealing:
kubectl create secret generic secret-name \
--dry-run=client \
--from-literal=foo=bar \
-o yaml | \
kubeseal \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
--format yaml \
--cert mycert.pem > mysealedsecret.yml
Example Sealed Secret
After running kubeseal, you’ll get an encrypted resource like this:
apiVersion : bitnami.com/v1alpha1
kind : SealedSecret
metadata :
name : exchange-router-secret
namespace : default
spec :
encryptedData :
DATABASE_URL : AgBqk7YP8n3J... # Encrypted value
API_KEY : AgCc5HxT9m2K... # Encrypted value
template :
metadata :
name : exchange-router-secret
namespace : default
type : Opaque
This encrypted SealedSecret can be safely committed to Git. Only the controller in your cluster can decrypt it.
Deploying Sealed Secrets
Apply the sealed secret to your cluster:
# Apply the sealed secret
kubectl apply -f sealed-secret.yml
# The controller automatically creates the corresponding Secret
kubectl get secret exchange-router-secret
# View the decrypted secret (base64 encoded)
kubectl get secret exchange-router-secret -o yaml
Using Secrets in Pods
Once deployed, use the decrypted secret like any Kubernetes secret:
apiVersion : apps/v1
kind : Deployment
metadata :
name : exchange-router
spec :
template :
spec :
containers :
- name : router
image : exchange-router:v1.0
envFrom :
# Inject all secrets as environment variables
- secretRef :
name : exchange-router-secret
Or mount specific keys:
spec :
containers :
- name : router
image : exchange-router:v1.0
env :
- name : DATABASE_URL
valueFrom :
secretKeyRef :
name : exchange-router-secret
key : DATABASE_URL
Sealing Scopes
Sealed Secrets supports three scopes that control where a sealed secret can be used:
Strict (Default)
Sealed secret can only be used in the same namespace with the same name:
kubeseal --scope strict < secret.yml > sealed-secret.yml
Namespace-Wide
Sealed secret can be used with any name in the same namespace:
kubeseal --scope namespace-wide < secret.yml > sealed-secret.yml
Cluster-Wide
Sealed secret can be used anywhere in the cluster:
kubeseal --scope cluster-wide < secret.yml > sealed-secret.yml
Use cluster-wide scope sparingly as it reduces security. Prefer strict scope for production secrets.
Secret Rotation
Updating Sealed Secrets
Create a new sealed secret with updated values:
kubectl create secret generic exchange-router-secret \
--dry-run=client \
--from-literal=DATABASE_URL= "postgresql://newuser:newpass@host:5432/db" \
-o yaml | kubeseal --format yaml > sealed-secret.yml
Apply the updated sealed secret:
kubectl apply -f sealed-secret.yml
Pods using the secret need to be restarted to pick up changes:
kubectl rollout restart deployment exchange-router
Certificate Rotation
The controller automatically generates new encryption keys every 30 days while keeping old keys for decryption:
# View all encryption keys
kubectl get secrets -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key
# Fetch the latest certificate
kubeseal --fetch-cert > latest-cert.pem
Management Commands
Inspect Sealed Secrets
# List all sealed secrets
kubectl get sealedsecrets --all-namespaces
# Describe a sealed secret
kubectl describe sealedsecret exchange-router-secret
# View corresponding regular secret
kubectl get secret exchange-router-secret -o yaml
Raw Value Encryption
Encrypt a single value without creating a full secret:
echo -n "my-secret-value" | kubeseal --raw \
--name=secret-name \
--namespace=default \
--from-file=/dev/stdin
Re-encryption
Re-encrypt an existing sealed secret (e.g., after certificate rotation):
kubeseal --re-encrypt < old-sealed-secret.yml > new-sealed-secret.yml
Best Practices
Never Commit Regular Secrets Always seal secrets before committing to version control. Add *-secret.yml to .gitignore
Use Strict Scope Default to strict scope for production secrets to limit where they can be used
Backup Encryption Keys Backup the controller’s encryption keys from the kube-system namespace
Automate in CI/CD Use offline certificates in CI/CD pipelines to seal secrets without cluster access
Troubleshooting
Sealed Secret Not Creating Secret
# Check controller logs
kubectl logs -n kube-system -l app.kubernetes.io/name=sealed-secrets
# Verify sealed secret status
kubectl get sealedsecret exchange-router-secret -o yaml
# Check for events
kubectl get events --field-selector involvedObject.name=exchange-router-secret
Certificate Fetch Fails
# Ensure controller is running
kubectl get pods -n kube-system -l app.kubernetes.io/name=sealed-secrets
# Check service
kubectl get svc -n kube-system sealed-secrets-controller
# Test connectivity
kubectl port-forward -n kube-system svc/sealed-secrets-controller 8080:8080
kubeseal --fetch-cert --controller-name=sealed-secrets-controller --controller-namespace=kube-system
Namespace/Name Mismatch
If you see “unable to decrypt” errors:
Ensure the namespace in the SealedSecret matches where it’s deployed
Verify the name matches exactly (case-sensitive)
Check the scope used when sealing
GitOps Workflow
Repository Structure
k8s/
├── base/
│ ├── deployment.yml
│ └── sealed-secret.yml # Encrypted, safe to commit
├── overlays/
│ ├── staging/
│ │ └── sealed-secret.yml
│ └── production/
│ └── sealed-secret.yml
└── scripts/
└── seal-secret.sh # Helper script
Seal Script Example
#!/bin/bash
# scripts/seal-secret.sh
if [ $# -lt 2 ]; then
echo "Usage: $0 <secret-name> <namespace>"
exit 1
fi
SECRET_NAME = $1
NAMESPACE = $2
kubectl create secret generic "${ SECRET_NAME }" \
--dry-run=client \
--namespace= "${ NAMESPACE }" \
--from-env-file=.env \
-o yaml | \
kubeseal \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
--format yaml > "k8s/overlays/${ NAMESPACE }/sealed-secret.yml"
echo "Sealed secret created at k8s/overlays/${ NAMESPACE }/sealed-secret.yml"
Additional Resources