DevOpsil

Sealed Secrets In GitOps: Managing Kubernetes Secrets Safely In Git Repositories

Sarah ChenSarah Chen4 min read

Sealed Secrets in GitOps: Quick Reference Guide

Managing Kubernetes secrets in Git is one of those problems that sounds simple until you accidentally push a base64-encoded password to a public repo. Sealed Secrets is the solution I recommend — encrypt once, store anywhere, decrypt only in-cluster.

What You Need

# Install kubeseal CLI
brew install kubeseal  # macOS
# or
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/kubeseal-0.24.0-linux-amd64.tar.gz

# Deploy the controller to your cluster
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
  --namespace kube-system \
  --set fullnameOverride=sealed-secrets-controller

Core Workflow

# 1. Create a regular secret (don't apply this!)
kubectl create secret generic my-secret \
  --from-literal=api-key=supersecretvalue \
  --dry-run=client -o yaml > my-secret.yaml

# 2. Seal it
kubeseal --format=yaml < my-secret.yaml > my-sealed-secret.yaml

# 3. Commit the sealed version — this is safe
git add my-sealed-secret.yaml
git commit -m "feat: add sealed API key secret"

# 4. Apply it (or let ArgoCD/Flux handle this)
kubectl apply -f my-sealed-secret.yaml

The controller decrypts it in-cluster and creates a standard Kubernetes Secret. Your pipeline never touches plaintext.

Scoping Options

Sealed Secrets supports three scopes — pick the right one for your use case.

ScopeDecryptable ByUse When
strict (default)Same name + namespace onlyProduction secrets
namespace-wideAny name in same namespaceFlexible namespace deployments
cluster-wideAnywhere in clusterShared infra secrets
# Namespace-wide scope
kubeseal --scope namespace-wide --format=yaml < my-secret.yaml > sealed.yaml

# Cluster-wide scope
kubeseal --scope cluster-wide --format=yaml < my-secret.yaml > sealed.yaml

Fetch the Public Key (CI/CD Pipelines)

Don't require cluster access at seal-time. Fetch the cert once and reuse it.

# Fetch and store the certificate
kubeseal --fetch-cert \
  --controller-name=sealed-secrets-controller \
  --controller-namespace=kube-system > pub-cert.pem

# Seal offline using the cert (no cluster connection needed)
kubeseal --cert pub-cert.pem --format=yaml < my-secret.yaml > sealed.yaml

Store pub-cert.pem in your repo. It's a public key — committing it is fine and intentional.

Multi-Environment Pattern

secrets/
├── pub-cert-staging.pem
├── pub-cert-production.pem
├── staging/
│   └── db-credentials.sealed.yaml
└── production/
    └── db-credentials.sealed.yaml
# Seal for staging
kubeseal --cert secrets/pub-cert-staging.pem \
  --format=yaml < db-creds.yaml > secrets/staging/db-credentials.sealed.yaml

# Seal for production
kubeseal --cert secrets/pub-cert-production.pem \
  --format=yaml < db-creds.yaml > secrets/production/db-credentials.sealed.yaml

Same plaintext secret, different ciphertext per environment. Neither can decrypt the other's secrets.

GitHub Actions Integration

- name: Seal Kubernetes Secret
  run: |
    # Create secret from GitHub secret
    kubectl create secret generic app-secret \
      --from-literal=DATABASE_URL=${{ secrets.DATABASE_URL }} \
      --dry-run=client -o yaml | \
    kubeseal \
      --cert ./pub-cert.pem \
      --format=yaml > manifests/app-secret.sealed.yaml

- name: Commit Sealed Secret
  run: |
    git config user.email "[email protected]"
    git config user.name "CI Bot"
    git add manifests/app-secret.sealed.yaml
    git diff --staged --quiet || git commit -m "chore: update sealed secrets"
    git push

Key Rotation

# The controller auto-rotates keys every 30 days by default
# Force a key rotation
kubectl label secret \
  -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key=active \
  sealedsecrets.bitnami.com/sealed-secrets-key=compromised

# Restart controller to generate new key
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system

# Re-seal all secrets with the new cert after fetching it
kubeseal --fetch-cert > new-pub-cert.pem

Important: Old sealed secrets still work because the controller retains old keys. Re-seal proactively after rotation for hygiene.

Common Commands

# Verify a sealed secret can be decrypted
kubeseal --validate < my-sealed-secret.yaml

# View the decrypted secret (requires cluster access)
kubectl get secret my-secret -o jsonpath='{.data.api-key}' | base64 -d

# List all sealing keys in the controller
kubectl get secrets -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key

# Check controller logs
kubectl logs -n kube-system \
  -l app.kubernetes.io/name=sealed-secrets

What to Watch Out For

  • Don't delete the controller's sealing keys — you'll lose the ability to decrypt existing secrets
  • Back up the sealing keys before cluster teardown: kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealing-keys-backup.yaml
  • Strict scope breaks on rename — if you rename a secret or move namespaces, you must re-seal
  • One secret per file — keeps diffs clean and avoids merge conflicts in Git

Sealed Secrets isn't the only option (SOPS and External Secrets are valid alternatives), but for teams going code-first with GitOps, it's the fastest path from "secrets everywhere" to "secrets auditable in Git." Commit the sealed file, let the controller handle the rest.

Share:

Was this article helpful?

Sarah Chen
Sarah Chen

CI/CD Engineering Lead

Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.

Related Articles

Discussion