Rotate a secret
This page walks through rotating any secret the cluster consumes — database passwords, API tokens, registry credentials, Cloudflare tokens, whatever. The mechanics are the same for all of them; only the target files differ.
When you rotate
- A credential leaked (or you strongly suspect it did — e.g. it showed up in a log, a pastebin, a screen-share).
- A colleague who had access left the team.
- You're doing a routine rotation because something is old. We don't run on a strict schedule, but anything more than a year old is a fair candidate.
- Bitnami rotated the sealed-secrets controller key (rare — you'd know, because many apps would start failing at once).
For 99% of operators, reaching for this page happens once or twice a year.
The flow
1. Update the source of truth (Bitwarden)
Rotate the password at the service that owns it — e.g. Docker Hub for regcred, Cloudflare for the API token, ALTER USER on MariaDB for a DB password. Copy the new value into the corresponding Bitwarden item.
2. Edit the plaintext YAML
Every secret the cluster uses has a plaintext Kubernetes Secret in untracked/secrets/<name>.yaml. Open it, paste the new value.
- 🟢 This file is gitignored — the whole
untracked/tree is. Plaintext never leaves your machine. - 🟠 Double-check you're editing
untracked/secrets/<name>.yaml, not something undermanifests_v1/— committing plaintext is the one mistake to avoid. - 🔴 Never share the file over Chat / email / screenshots. Bitwarden is the sharing channel.
3. Seal it
From the repo root:
cd untracked/secrets
./seal.shseal.sh is a short shell script with one seal <file> or cluster_seal <file> line per secret we manage. It shells out to kubeseal, which contacts the in-cluster sealed-secrets-controller to fetch the public key, then writes the encrypted form to sealed-<name>.yaml alongside the plaintext. The scope matters:
seal— namespace-scoped. The sealed file can only decrypt in the namespace named in its metadata.cluster_seal— cluster-wide. Any namespace can decrypt it. Used for things likeregcredthat multiple tenants share.
4. Copy the sealed file to its committed location
The sealed output stays in untracked/ by default; you have to move it into place. Mapping examples:
| Secret | Destination |
|---|---|
regcred | manifests_v1/app-constructs/ecommercen-clients/wecare/adveshop4/base/sealed-regcred.yaml (and two other bases — it's cluster-wide) |
tunnel-credentials | manifests_v1/app-constructs/cloudflared/sealed-tunnel-credentials.yaml |
certmanager-cloudflare-token | manifests_v1/app-constructs/cert-manager/setup/issuers/sealed-certmanager-cloudflare-token.yaml |
wecare/app-db-secrets | manifests_v1/app-constructs/ecommercen-clients/wecare/adveshop4/base/sealed-app-db-secrets.yaml |
Shared cluster-wide secrets (regcred, redis-auth-secret, ecn-bucket-access) live in more than one place — the full mapping is documented in the secrets-manager agent's knowledge base. Forgetting one of the copies shows up as an ImagePullBackOff or an auth error in a single tenant.
5. Commit and push
git add manifests_v1/app-constructs/<app>/sealed-<name>.yaml
git commit -m "[App: <app>] Rotate <secret-name>"
git pushArgoCD syncs the SealedSecret resource within a minute. The sealed-secrets controller decrypts it into a real Kubernetes Secret. Stakater Reloader notices the Secret's resourceVersion changed and rolling-restarts every Deployment / StatefulSet that references it. No manual kubectl rollout restart needed.
Worked example — rotating regcred
regcred is our Docker Hub pull token. It's cluster-wide and referenced in every tenant's imagePullSecrets.
# 1. Generate a new token on Docker Hub, update the Bitwarden entry "ecnv4 - dockerhub regcred"
# 2. Update the plaintext
$EDITOR untracked/secrets/regcred.yaml # paste new .dockerconfigjson value
# 3. Seal
cd untracked/secrets && ./seal.sh
# 4. Copy to all three locations (cluster-wide secret used by multiple tenants)
cp sealed-regcred.yaml ../../manifests_v1/app-constructs/ecommercen/bases/adveshop4/
cp sealed-regcred.yaml ../../manifests_v1/app-constructs/ecommercen-saas/base/
cp sealed-regcred.yaml ../../manifests_v1/app-constructs/ecommercen-clients/wecare/adveshop4/base/
# 5. Commit + push
cd ../..
git add manifests_v1/app-constructs/**/sealed-regcred.yaml
git commit -m "[Infra] Rotate regcred Docker Hub token"
git pushSpecial cases
- 🟠
argocd-secretis patched imperatively in-cluster, not via Sealed Secrets. ArgoCD's server config owns the admin password, and sealing it creates a reconcile loop with ArgoCD's own controller. If you need to rotate it, ask theargocd-manageragent — it knows thekubectl patchpath. - 🟠 Autoscaler config goes through
untracked/secrets/build-autoscaler-config.sh, notseal.sh. It pulls the RKE2 join token from Bitwarden, assembles the per-pool JSON, and seals directly into the manifest tree. Requires the Bitwarden CLI unlocked. - 🟢 Sealed output differs every time even if plaintext is unchanged. kubeseal adds a random nonce. That's fine — the committed file just gets a new encrypted blob; decrypted value is the same.
- 🔴 Never re-key the controller without a full re-seal plan. The old sealed files become unreadable. If this ever comes up, the
secrets-manageragent handles it.
The easy path: delegate to the agent
The secrets-manager agent owns this flow end-to-end. You tell it what you want rotated; it edits the plaintext, runs the seal, copies to all the right places, and hands back a ready-to-commit diff. Unless you want the manual practice, just say something like:
"Rotate the cloudflared tunnel credentials."
and let it drive.
Further reading
- Secrets & Bitwarden — the conceptual model
- Rules & guardrails — the "never commit plaintext" rule lives here
- Claude agents — the
secrets-manageragent entry - Scripts —
seal.shandbuild-autoscaler-config.shreference