Manage Sealed Secrets#

This project uses Sealed Secrets to store encrypted secrets in the Git repository. The sealed-secrets controller in the cluster decrypts them at deploy time.

How it works#

        flowchart TD
    A[Plain Secret] -->|kubeseal| B[SealedSecret YAML]
    B -->|git push| C[Git Repository]
    C -->|ArgoCD sync| D[Cluster]
    D -->|sealed-secrets controller| E[Decrypted Secret]
    
  1. You create a standard Kubernetes Secret (dry-run, not applied to the cluster).

  2. kubeseal encrypts it using the cluster’s public key — only that specific cluster can decrypt it.

  3. The resulting SealedSecret YAML is safe to commit to Git.

  4. ArgoCD syncs the SealedSecret to the cluster.

  5. The sealed-secrets controller decrypts it into a regular Secret.

Existing SealedSecrets#

Important

SealedSecrets are encrypted for a specific cluster. If you forked this repo, the existing sealed secret files will not decrypt on your cluster — the sealed-secrets controller will log errors but the cluster will otherwise run normally. You must create your own secrets by following Set Up DNS, TLS & Cloudflare Tunnel and Set Up OAuth Authentication.

These secrets are created during the setup guides:

Secret

Namespace

Purpose

File

cloudflared-credentials

cloudflared

Cloudflare tunnel token

additions/cloudflared/tunnel-secret.yaml

cloudflare-api-token

cert-manager

DNS-01 API token

additions/cert-manager/templates/cloudflare-api-token-secret.yaml

oauth2-proxy-secret

oauth2-proxy

OAuth2 cookie + client secrets

additions/oauth2-proxy/oauth2-proxy-secret.yaml

Prerequisites#

  • kubeseal is installed in the devcontainer (via the tools role)

  • The sealed-secrets controller is running in the cluster (deployed by ArgoCD)

  • kubectl has access to the cluster (kubeconfig configured)

Create a new SealedSecret#

From a literal value#

# Prompt for the secret value (not echoed, not stored in shell history)
printf 'Secret value: ' && read -rs SECRET_VALUE && echo

# Create the SealedSecret
printf '%s' "$SECRET_VALUE" | \
  kubectl create secret generic my-secret-name \
    --namespace my-namespace \
    --from-file=my-key=/dev/stdin \
    --dry-run=client -o yaml | \
  kubeseal --controller-name sealed-secrets \
    --controller-namespace kube-system -o yaml > \
    kubernetes-services/additions/my-service/my-secret.yaml

unset SECRET_VALUE

From a file#

kubectl create secret generic my-secret-name \
  --namespace my-namespace \
  --from-file=my-key=path/to/secret-file \
  --dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets \
  --controller-namespace kube-system -o yaml > \
  kubernetes-services/additions/my-service/my-secret.yaml

With multiple keys#

kubectl create secret generic my-secret-name \
  --namespace my-namespace \
  --from-literal=username=admin \
  --from-literal=password=supersecret \
  --dry-run=client -o yaml | \
kubeseal --controller-name sealed-secrets \
  --controller-namespace kube-system -o yaml > \
  kubernetes-services/additions/my-service/my-secret.yaml

Commit and deploy#

git add kubernetes-services/additions/my-service/my-secret.yaml
git commit -m "Add my-secret SealedSecret"
git push

ArgoCD syncs the SealedSecret automatically.

Rotate a secret#

To update an existing SealedSecret with a new value:

  1. Re-run the kubeseal command above, overwriting the existing file.

  2. Commit and push.

  3. ArgoCD syncs the updated SealedSecret.

  4. The sealed-secrets controller replaces the decrypted Secret.

  5. Restart any pods that use the secret to pick up the new value:

kubectl rollout restart deployment/my-deployment -n my-namespace

Troubleshooting#

SealedSecret not decrypting#

Check the sealed-secrets controller logs:

kubectl logs -n kube-system deployment/sealed-secrets -f

Common issues:

  • Wrong controller name/namespace — ensure kubeseal flags match the deployed controller (--controller-name sealed-secrets --controller-namespace kube-system).

  • Wrong cluster — SealedSecrets are encrypted for a specific cluster. A SealedSecret created against one cluster cannot be decrypted by another.

  • Namespace mismatch — by default, SealedSecrets are scoped to the namespace specified at creation time. The Secret must be deployed to the same namespace.

Re-seal after cluster rebuild#

If you rebuild the cluster (new sealed-secrets controller = new keypair), all existing SealedSecrets become undecryptable. You must re-seal every secret:

  1. Retrieve the original plain-text values.

  2. Re-run kubeseal against the new cluster.

  3. Commit and push the updated SealedSecret files.

Tip

Back up the sealed-secrets controller’s private key if you want to preserve the ability to re-use existing SealedSecrets after a rebuild:

kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > sealed-secrets-key-backup.yaml

Store this backup securely (not in Git!) — it can decrypt all your SealedSecrets.

Gotchas#

Encryption is bound to name and namespace#

SealedSecrets are encrypted for a specific Secret name and namespace. You cannot rename a SealedSecret YAML file’s metadata.name or metadata.namespace and expect it to decrypt — you must re-run kubeseal with the new name/namespace.

Merging into existing Secrets#

If a Secret already exists in the cluster (e.g., created by a Helm chart) and you want the sealed-secrets controller to manage it, add this annotation to the existing Secret:

metadata:
  annotations:
    sealedsecrets.bitnami.com/managed: "true"

Without this annotation, the controller will not overwrite the existing Secret.

Avoid sealing into argocd-secret#

The ArgoCD argocd-secret in the argo-cd namespace is managed by ArgoCD itself. Sealing values into it causes conflicts — ArgoCD and the sealed-secrets controller fight over the Secret contents. Use a separate SealedSecret and mount it where needed instead.

File naming for gitleaks#

SealedSecret files must be named *-secret.yaml (singular) to match the .gitleaks.toml allowlist. Files named *-secrets.yaml (plural) will be flagged as leaked secrets by the pre-commit hook.