secrets-management
A leaked secret isn't a bug you fix by deleting a line — it's a credential an attacker may already have, in a place (git history, an image layer, a log aggregator) that keeps it long after you "removed" it. This skill keeps secrets out of those places to begin with, and handles the leak-response when one gets in anyway.
The injection hierarchy (best → worst)
- No long-lived secret at all — workload identity / OIDC federation. The workload proves who it is and gets a short-lived token minted on demand. Nothing static to leak. Prefer this everywhere it's available (cloud SDKs, CI→cloud, service→service).
- A secrets manager (Vault, AWS/GCP Secrets Manager, SOPS-encrypted files) the app reads at startup or on demand, authenticated by workload identity. Central rotation, audit log, access control.
- Injected environment variables / mounted files from the platform's secret store (K8s Secret mounted as a file > as an env var — env vars leak into child processes, crash dumps, and
/proc).
- A committed
.env file — only for local dev, never for shared/prod, and .env is in .gitignore from the first commit.
Never: a secret hardcoded in source, in a client-side bundle (anything shipped to a browser/app is public — there are no frontend secrets), in a Dockerfile ENV/ARG, in a committed config file, or in CI plaintext where a fork PR could read it.
Keeping them out
.gitignore / .dockerignore before the first commit. .env, *.pem, *.key, credential JSONs, *.tfvars with secrets.
- A pre-commit secret scanner (gitleaks/trufflehog) so a key never reaches a commit. Add the same scan in CI as a backstop.
- Redact in logs. Build a logging serializer that masks known-sensitive keys (
authorization, password, token, secret, api_key) — don't rely on remembering at each call site. Assume logs ship to a third-party backend.
- Not in images/state: no secret in a Docker layer (use BuildKit secret mounts), no secret in Terraform state committed to git (state holds secrets in plaintext — encrypt the backend, keep it out of git).
- Not in URLs. A secret in a query string lands in access logs, browser history, and referrer headers. Use headers/body.
When a secret leaks — the response
Order matters. Do these in sequence:
- Rotate/revoke first. The secret is compromised the moment it's exposed. Invalidate it and issue a new one before anything else. Scrubbing history without rotating is theater — the value is already out.
- Assess exposure. Where did it go — public repo, a log aggregator, an image on a registry, a Slack message? Each is a separate copy to deal with.
- Purge from history if needed (
git filter-repo / BFG) and force-push — but only after rotation, and know that anyone who cloned/forked, and GitHub's cached views, may still have it. History rewrite is cleanup, not containment.
- Audit for use. Check access logs for the credential between exposure and revocation.
- Add the guard that would have caught it (pre-commit scanner, review rule) so it doesn't recur.
"Removed in a later commit/layer" ≠ gone. A secret committed and then deleted is still in git history. A secret added and rm-ed in a later Docker layer is still in the earlier layer. The only fix is rotation + purge, not deletion.
Rotation
- Secrets have a lifecycle. Rotate on a schedule and on personnel changes, not just after incidents. Short-lived (hours) beats long-lived (years) — the shorter the life, the smaller the leak window.
- Design for zero-downtime rotation: support two valid secrets at once (old + new) during the cutover so rotating doesn't cause an outage. A secret you can't rotate without downtime won't get rotated.
- Prefer systems that rotate automatically (managed DB credentials, dynamic secrets from Vault) over manual rotation you'll forget.
Procedure
- Classify how the secret should be delivered using the hierarchy — push toward workload identity / short-lived.
- Wire the app to read from the secret manager / injected mount, authenticated by workload identity; remove any hardcoded/committed value.
- Ensure
.gitignore/.dockerignore cover it; add a pre-commit + CI secret scanner; add log redaction.
- Confirm it's absent from source, client bundles, image layers, Terraform-state-in-git, URLs, and logs.
- Set up rotation (scheduled or automatic) with dual-secret cutover.
- If responding to a leak: rotate → assess → purge → audit → add the guard, in that order.
Definition of done
- The secret is delivered by the highest-feasible tier (ideally short-lived/workload identity), never hardcoded or committed.
- Ignore files + pre-commit + CI scanning prevent commits; logs redact sensitive keys.
- Not present in source, client bundle, image layers, state-in-git, URLs, or logs.
- A rotation path exists and supports overlap (no-downtime cutover).
- Leak response, if any, rotated before scrubbing and audited access.
1---2name: secrets-management3description: Keep secrets out of code, logs, images, and state — and make them rotatable. Use when adding a credential/API key/token to any system, when a secret leaks, or when auditing how an app gets its secrets. Covers the injection hierarchy (short-lived > secret manager > env), git-history remediation, rotation, and the "removed but still in history" trap.4---56# secrets-management78A leaked secret isn't a bug you fix by deleting a line — it's a credential an attacker may already have, in a place (git history, an image layer, a log aggregator) that keeps it long after you "removed" it. This skill keeps secrets out of those places to begin with, and handles the leak-response when one gets in anyway.910## The injection hierarchy (best → worst)111. **No long-lived secret at all** — workload identity / OIDC federation. The workload proves who it is and gets a short-lived token minted on demand. Nothing static to leak. Prefer this everywhere it's available (cloud SDKs, CI→cloud, service→service).122. **A secrets manager** (Vault, AWS/GCP Secrets Manager, SOPS-encrypted files) the app reads at startup or on demand, authenticated by workload identity. Central rotation, audit log, access control.133. **Injected environment variables / mounted files** from the platform's secret store (K8s Secret mounted as a file > as an env var — env vars leak into child processes, crash dumps, and `/proc`).144. **A committed `.env` file** — only for local dev, never for shared/prod, and `.env` is in `.gitignore` from the first commit.1516Never: a secret hardcoded in source, in a client-side bundle (anything shipped to a browser/app is public — there are no frontend secrets), in a Dockerfile `ENV`/`ARG`, in a committed config file, or in CI plaintext where a fork PR could read it.1718## Keeping them out19- **`.gitignore` / `.dockerignore` before the first commit.** `.env`, `*.pem`, `*.key`, credential JSONs, `*.tfvars` with secrets.20- **A pre-commit secret scanner** (gitleaks/trufflehog) so a key never reaches a commit. Add the same scan in CI as a backstop.21- **Redact in logs.** Build a logging serializer that masks known-sensitive keys (`authorization`, `password`, `token`, `secret`, `api_key`) — don't rely on remembering at each call site. Assume logs ship to a third-party backend.22- **Not in images/state:** no secret in a Docker layer (use BuildKit secret mounts), no secret in Terraform state committed to git (state holds secrets in plaintext — encrypt the backend, keep it out of git).23- **Not in URLs.** A secret in a query string lands in access logs, browser history, and referrer headers. Use headers/body.2425## When a secret leaks — the response26Order matters. Do these in sequence:271. **Rotate/revoke first.** The secret is compromised the moment it's exposed. Invalidate it and issue a new one *before* anything else. Scrubbing history without rotating is theater — the value is already out.282. **Assess exposure.** Where did it go — public repo, a log aggregator, an image on a registry, a Slack message? Each is a separate copy to deal with.293. **Purge from history if needed** (`git filter-repo` / BFG) and force-push — but only *after* rotation, and know that anyone who cloned/forked, and GitHub's cached views, may still have it. History rewrite is cleanup, not containment.304. **Audit for use.** Check access logs for the credential between exposure and revocation.315. **Add the guard that would have caught it** (pre-commit scanner, review rule) so it doesn't recur.3233**"Removed in a later commit/layer" ≠ gone.** A secret committed and then deleted is still in git history. A secret added and `rm`-ed in a later Docker layer is still in the earlier layer. The only fix is rotation + purge, not deletion.3435## Rotation36- **Secrets have a lifecycle.** Rotate on a schedule and on personnel changes, not just after incidents. Short-lived (hours) beats long-lived (years) — the shorter the life, the smaller the leak window.37- **Design for zero-downtime rotation:** support two valid secrets at once (old + new) during the cutover so rotating doesn't cause an outage. A secret you can't rotate without downtime won't get rotated.38- Prefer systems that rotate automatically (managed DB credentials, dynamic secrets from Vault) over manual rotation you'll forget.3940## Procedure411. Classify how the secret should be delivered using the hierarchy — push toward workload identity / short-lived.422. Wire the app to read from the secret manager / injected mount, authenticated by workload identity; remove any hardcoded/committed value.433. Ensure `.gitignore`/`.dockerignore` cover it; add a pre-commit + CI secret scanner; add log redaction.444. Confirm it's absent from source, client bundles, image layers, Terraform-state-in-git, URLs, and logs.455. Set up rotation (scheduled or automatic) with dual-secret cutover.466. If responding to a leak: rotate → assess → purge → audit → add the guard, in that order.4748## Definition of done49- The secret is delivered by the highest-feasible tier (ideally short-lived/workload identity), never hardcoded or committed.50- Ignore files + pre-commit + CI scanning prevent commits; logs redact sensitive keys.51- Not present in source, client bundle, image layers, state-in-git, URLs, or logs.52- A rotation path exists and supports overlap (no-downtime cutover).53- Leak response, if any, rotated *before* scrubbing and audited access.