The ‘Keys Under the Mat’ Problem in GitOps
GitOps has fundamentally changed how we deploy to Kubernetes. Whether you use ArgoCD or Flux, syncing your cluster state with a Git repository feels like magic. But secrets remain the elephant in the room. Standard Kubernetes Secrets are merely Base64 encoded—not encrypted. Committing them to Git is the security equivalent of leaving your front door keys under the mat and hoping nobody looks.
I spent months hunting for a ‘Goldilocks’ solution that sits between the overkill of HashiCorp Vault and the dangerous manual ‘kubectl apply’ method. After running Bitnami Sealed Secrets in production for 8 months—managing over 150 secrets across 12 namespaces—I’m convinced it is the sweet spot for most engineering teams.
This approach allows you to treat sensitive data exactly like your code: versioned, audited, and automatically deployed.
Under the Hood: How Encryption Works
Sealed Secrets uses asymmetric encryption to solve the secret-in-Git dilemma. The system relies on two main components:
- The Controller: A pod running in your cluster that holds the private key. It is the only entity that can turn encrypted data back into usable secrets.
- The CLI (kubeseal): A tool on your local machine that uses the controller’s public key to encrypt your data.
The result is a SealedSecret custom resource. You can safely commit this file to a public GitHub repo. Even if a bad actor gains access to the repository, they cannot decrypt the values without the private key stored inside your cluster’s memory.
Step 1: Deployment and Setup
Helm is the path of least resistance for installation. I recommend deploying into the kube-system namespace to isolate the controller from your standard application workloads.
# Add the Bitnami repository
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
# Install the controller
helm install sealed-secrets-controller sealed-secrets/sealed-secrets -n kube-system
Grab the kubeseal binary for your local environment. On Linux, this one-liner fetches the latest version directly from GitHub:
KUBESEAL_VERSION=$(curl -s https://api.github.com/repos/bitnami-labs/sealed-secrets/releases/latest | grep tag_name | cut -d '"' -f 4 | sed 's/v//')
wget "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
sudo install -m 755 kubeseal /usr/local/bin/kubeseal
Step 2: The Workflow in Action
The process is straightforward: create a temporary local secret, seal it, and then delete the original. Let’s look at a database credential example.
Generate the YAML
First, we create a standard Secret without applying it to the cluster. We use --dry-run=client to pipe the output into a file.
kubectl create secret generic db-credentials \
--from-literal=password='super-secret-password' \
--dry-run=client -o yaml > db-secret.yaml
Seal the Data
Encryption happens next. The kubeseal tool communicates with your cluster to fetch the public key, then transforms your secret into a SealedSecret.
kubeseal < db-secret.yaml --format yaml > db-sealed-secret.yaml
Now, delete db-secret.yaml immediately. The remaining db-sealed-secret.yaml file is now the “source of truth” for your GitOps pipeline.
Step 3: A Critical Detail: Understanding Scopes
One nuance that tripped me up early on was “Scope.” Sealed Secrets provides three security boundaries:
- strict (default): The secret is tied to a specific name and namespace. If you move it, it won’t decrypt.
- namespace-wide: You can rename the secret, but it must stay in the same namespace.
- cluster-wide: The secret can be unsealed anywhere in the cluster.
Stick to strict for production. It prevents a developer from accidentally (or intentionally) unsealing a production database password in a development namespace.
Verification and Troubleshooting
After applying the file to your cluster, the controller works behind the scenes to create the standard Secret. Verify the sync status with this command:
kubectl get sealedsecret db-credentials
# Look for: Condition: Synced
Troubleshooting is usually a matter of checking the controller logs. If a secret isn’t appearing, the logs in kube-system will tell you if there’s a scope mismatch or a decryption error.
kubectl logs -n kube-system -l app.kubernetes.io/name=sealed-secrets
Disaster Recovery: The ‘Don’t Lose Your Keys’ Rule
The private key is the soul of your encryption. If your cluster dies and you haven’t backed up the master key, your Git repository becomes a graveyard of useless encrypted files. I always store a copy of the master key in a secure corporate vault.
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > master-key-backup.yaml
Final Verdict
Switching to Sealed Secrets stopped the ‘who has the production secret file?’ Slack messages and cleared a major bottleneck for our team. It fits perfectly into a Pull Request workflow where every change is audited and visible. While massive enterprises might require the features of HashiCorp Vault, Sealed Secrets offers the best balance of security and speed for growing dev teams.

