Overview
Sealed Secrets allows you to encrypt Kubernetes Secret resources so they can be safely stored in Git repositories. The controller decrypts them into regular Secrets when deployed to the cluster.
How It Works
Encrypt locally
Use kubeseal CLI to encrypt a Secret using the cluster’s public key
Store in Git
Commit the encrypted SealedSecret to your Git repository
Deploy with GitOps
Flux applies the SealedSecret to the cluster
Automatic decryption
The sealed-secrets controller decrypts it into a regular Secret
HelmRelease Configuration
Sealed Secrets is deployed using Flux with the following HelmRelease:
apiVersion : helm.toolkit.fluxcd.io/v2
kind : HelmRelease
metadata :
name : sealed-secrets
spec :
chart :
spec :
chart : sealed-secrets
sourceRef :
kind : HelmRepository
name : sealed-secrets
version : "=2.17.3"
# https://github.com/bitnami-labs/sealed-secrets/releases
interval : 24h
releaseName : sealed-secrets-controller
install :
crds : Create
upgrade :
crds : CreateReplace
# https://github.com/bitnami-labs/sealed-secrets/blob/main/helm/sealed-secrets/values.yaml
values :
metrics :
serviceMonitor :
enabled : false
namespace : sealed-secrets
Configuration Parameters
Sealed Secrets chart version. Currently using 2.17.3
How often Flux checks for chart updates
spec.values.metrics.serviceMonitor.enabled
Enable Prometheus ServiceMonitor for metrics collection
Installing kubeseal CLI
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.3/kubeseal-0.27.3-linux-amd64.tar.gz
tar -xvzf kubeseal-0.27.3-linux-amd64.tar.gz
sudo install -m 755 kubeseal /usr/local/bin/kubeseal
Or download from GitHub Releases
Creating Sealed Secrets
Method 1: From existing Secret
Create a regular Secret
apiVersion : v1
kind : Secret
metadata :
name : database-credentials
namespace : production
type : Opaque
stringData :
username : admin
password : super-secret-password
Seal the Secret
kubeseal -f secret.yaml -w sealed-secret.yaml
This creates an encrypted SealedSecret resource
Commit to Git
git add sealed-secret.yaml
git commit -m "Add database credentials"
git push
Never commit the unencrypted secret.yaml to Git! Delete it after sealing.
Method 2: Direct command
Create a SealedSecret directly without intermediate files:
kubectl create secret generic database-credentials \
--dry-run=client \
--from-literal=username=admin \
--from-literal=password=super-secret-password \
-o yaml | \
kubeseal -o yaml > sealed-secret.yaml
Method 3: From file
Seal secrets from files:
kubectl create secret generic tls-cert \
--dry-run=client \
--from-file=tls.crt=cert.pem \
--from-file=tls.key=key.pem \
-o yaml | \
kubeseal -o yaml > sealed-tls-secret.yaml
SealedSecret Example
A SealedSecret looks like this:
apiVersion : bitnami.com/v1alpha1
kind : SealedSecret
metadata :
name : database-credentials
namespace : production
spec :
encryptedData :
username : AgBvXm1k5F3d...(encrypted data)...==
password : AgCQRv8Hj6Kw...(encrypted data)...==
template :
metadata :
name : database-credentials
namespace : production
type : Opaque
When applied to the cluster, the controller automatically creates:
apiVersion : v1
kind : Secret
metadata :
name : database-credentials
namespace : production
type : Opaque
data :
username : YWRtaW4=
password : c3VwZXItc2VjcmV0LXBhc3N3b3Jk
Scopes
Sealed Secrets support different scopes controlling where they can be decrypted:
Strict (default)
Namespace-wide
Cluster-wide
Sealed secret is bound to the exact namespace and name: kubeseal --scope strict -f secret.yaml -w sealed-secret.yaml
Characteristics:
Most secure
Cannot rename the secret
Cannot move to different namespace
Use case: Production secrets with strict access controlSealed secret can be renamed but must stay in the same namespace: kubeseal --scope namespace-wide -f secret.yaml -w sealed-secret.yaml
Characteristics:
Can rename the secret
Must stay in original namespace
Use case: Shared secrets within a namespaceSealed secret can be used anywhere in the cluster: kubeseal --scope cluster-wide -f secret.yaml -w sealed-secret.yaml
Characteristics:
Can rename the secret
Can move between namespaces
Less secure
Use case: Truly cluster-wide configuration (use sparingly)
Key Management
Fetch the public key
Download the cluster’s public key for offline sealing:
kubeseal --fetch-cert > public-key.pem
Then use it to seal secrets without cluster access:
kubeseal --cert public-key.pem -f secret.yaml -w sealed-secret.yaml
Store the public key in your repository so team members can create sealed secrets without cluster access.
Backup encryption keys
Backup the controller’s private keys:
kubectl get secret -n sealed-secrets -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealed-secrets-keys.yaml
Store this backup securely! If you lose the encryption keys, you cannot decrypt existing SealedSecrets.
Restore encryption keys
To restore keys on a new cluster:
kubectl apply -f sealed-secrets-keys.yaml
kubectl delete pod -n sealed-secrets -l app.kubernetes.io/name=sealed-secrets
Using Sealed Secrets in Applications
Once decrypted, use the Secret like any other Kubernetes Secret:
deployment.yaml
volume-mount.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : my-app
spec :
template :
spec :
containers :
- name : app
image : my-app:latest
env :
- name : DB_USERNAME
valueFrom :
secretKeyRef :
name : database-credentials
key : username
- name : DB_PASSWORD
valueFrom :
secretKeyRef :
name : database-credentials
key : password
Verifying Sealed Secrets
Check controller status
kubectl get pods -n sealed-secrets
Expected output:
NAME READY STATUS RESTARTS AGE
sealed-secrets-controller-xxxxx-xxxxx 1/1 Running 0 5d
View controller logs
kubectl logs -n sealed-secrets -l app.kubernetes.io/name=sealed-secrets
Check if Secret was created
kubectl get secret database-credentials -n production
Verify Secret content (base64 decoded)
kubectl get secret database-credentials -n production -o jsonpath='{.data.username}' | base64 -d
Troubleshooting
SealedSecret not decrypting
Check SealedSecret exists
kubectl get sealedsecrets -A
Check controller logs
kubectl logs -n sealed-secrets -l app.kubernetes.io/name=sealed-secrets
Look for decryption errors
Verify namespace and name match
With strict scope, namespace and name must exactly match the original
Check public/private key match
kubeseal --fetch-cert | openssl x509 -noout -fingerprint
Rotating secrets
To update a sealed secret:
Create new Secret with updated values
Seal it with kubeseal
Apply the new SealedSecret (it will replace the old one)
The controller automatically updates the decrypted Secret
# Create updated secret
kubectl create secret generic database-credentials \
--dry-run=client \
--from-literal=username=admin \
--from-literal=password=new-password \
-o yaml | \
kubeseal -o yaml > sealed-secret-updated.yaml
# Apply the update
kubectl apply -f sealed-secret-updated.yaml
GitOps Workflow
Typical workflow with Flux:
# 1. Create and seal secret
kubectl create secret generic app-config \
--dry-run=client \
--from-literal=api-key=abc123 \
-n production \
-o yaml | kubeseal -o yaml > overlays/production/app-config.sealed.yaml
# 2. Add to kustomization
cat >> overlays/production/kustomization.yaml << EOF
resources:
- app-config.sealed.yaml
EOF
# 3. Commit and push
git add overlays/production/
git commit -m "Add app configuration secret"
git push
# 4. Flux automatically applies the SealedSecret
# 5. Controller decrypts it into a regular Secret
# 6. Your application can use the Secret
Best Practices
Use strict scope for production secrets
Never commit unencrypted secrets to Git
Backup sealed-secrets encryption keys securely
Store the public key in your repository for team access
Use descriptive names for secrets
Document what each sealed secret contains in your repository
Rotate secrets regularly
Use separate SealedSecrets per environment (dev, staging, prod)
Resources