Skip to main content

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

  1. Controller: Runs in your cluster with a private key
  2. Public Certificate: Used by kubeseal CLI to encrypt secrets
  3. Encryption: Your secrets are encrypted locally using the public key
  4. Storage: Encrypted SealedSecrets can be safely stored in Git
  5. 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

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

  1. 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
  1. Apply the updated sealed secret:
kubectl apply -f sealed-secret.yml
  1. 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

Build docs developers (and LLMs) love