Sealed Secrets In GitOps: Managing Kubernetes Secrets Safely In Git Repositories
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.
| Scope | Decryptable By | Use When |
|---|---|---|
strict (default) | Same name + namespace only | Production secrets |
namespace-wide | Any name in same namespace | Flexible namespace deployments |
cluster-wide | Anywhere in cluster | Shared 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.
Was this article helpful?
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
ArgoCD Image Updater for Automated Container Deployments
Configure ArgoCD Image Updater to automatically detect and deploy new container images to Kubernetes without manual manifest changes or CI triggers.
Flux Notification Controller: Alerting On GitOps Drift And Sync Failures
When your GitOps pipeline breaks silently, you find out the worst way — during an incident. The Flux Notification Controller is your early warning system....
GitOps in Practice: ArgoCD vs Flux for Kubernetes Deployments
Side-by-side comparison of ArgoCD and Flux for GitOps on Kubernetes — architecture differences, sync behavior, multi-cluster, and when to choose each.
ArgoCD Application Patterns: App of Apps, ApplicationSets, and Beyond
Practical ArgoCD patterns for managing dozens of applications — from App of Apps to ApplicationSets to multi-cluster rollouts. All in code, obviously.
Git Worktrees For Managing Multiple Feature Branches Simultaneously
Stop stashing. Stop context-switching. Git worktrees let you check out multiple branches simultaneously in separate directories — each with its own working...
Vault Kubernetes Authentication: Injecting Secrets into Pods Without Hardcoding
Set up Vault's Kubernetes auth method so pods authenticate using their ServiceAccount tokens — then inject secrets via Vault Agent sidecar or the Vault Secrets Operator. Zero hardcoded credentials in your cluster.