Kubernetes Secrets in Git: A Field Guide to Bitnami Sealed Secrets

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

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:

  1. strict (default): The secret is tied to a specific name and namespace. If you move it, it won’t decrypt.
  2. namespace-wide: You can rename the secret, but it must stay in the same namespace.
  3. 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.

Share: