Secrets & Key-Scope Audit + Rotation Plan
Degree of freedom: MIXED — prefix scan is exact; rotate-vs-relocate
is judgment. Stay plan-only. Never print secret values.
This skill vs neighbors
| Skill |
Owns |
| plan-secrets-audit (this) |
Rotate vs relocate leaked keys |
plan-security-audit |
OWASP umbrella (not the first secrets match) |
plan-rls-audit |
Table access control |
audit-env-parity |
Env/config drift |
How to reason (every plan item)
- Propose — rotate, relocate, or hygiene — say which, never print the value
- Risk — who can use the key if it stays live (history counts)
- Keep-working — keys that are scoped correctly and not in history
- Phase — rotate → relocate → hygiene → optional scrub (do not execute)
Worked example
Propose: rotate the Stripe secret at api/pay.ts (last-4 only in the report); history presence means rotate, not .env move.
Risk: sk_ in committed history stays valid forever after a relocate.
Keep-working: Stripe pk_ in the client is publishable by design (still note the protector).
Phase: Phase 1 — rotate exposed never-client secrets.
Redaction: type + path + last 4 — never the secret.
Role: Senior security engineer (credential exposure + key scoping).
Task: Scan working tree and git history, classify each credential (rotate vs
relocate), score by scope and permanence, emit plan-secrets-audit.md. Audit & plan
only — no rotation, scrubbing, or env edits until each phase is approved.
Find every leaked key. Decide rotate vs relocate. Change nothing until approved.
A widely reported breach started with a hardcoded Supabase key in client JavaScript — combined
with RLS off, the public key became an admin backdoor. A secret committed even once
lives in git history forever. Moving it to .env later does nothing; the only real
fix is rotation.
When this fires
Trigger phrases: "scan for secrets", "are my keys exposed", "did I commit an
API key", "is my .env safe", "rotate keys", "about to open-source this",
"pre-launch secret check".
Do not fire for: RLS policy correctness (plan-rls-audit), input/webhook
validation (plan-input-validation). This skill owns credential exposure and
key scoping specifically.
Why a dedicated skill
A grep finds strings. This skill adds the two judgments a grep can't: scope
(is this key supposed to be client-side?) and permanence (is it in history,
making relocation insufficient?).
The audit
A · Pattern scan (working tree) [LOW freedom — run exactly]
Search outside .env* and server-only contexts for:
- Prefixes:
sk_, pk_, whsec_, service_role, eyJ..., AKIA, API_KEY,
SECRET, TOKEN, long random blobs.
- Supabase: anon vs
service_role.
- Any key in files that ship to the browser.
B · Scope classification [HIGH freedom]
- Safe client-side: Supabase anon, Stripe publishable (
pk_), public
analytics keys — note the protector dependency (RLS, Stripe design).
- Never client-side (Critical if exposed): service_role, Stripe secret
(
sk_), webhook secret (whsec_), AWS secrets, DB URLs.
NEXT_PUBLIC_ / VITE_ / EXPO_PUBLIC_ trap — bundled into client.
C · Git history (rotate vs relocate) [HIGH freedom]
- Ever in committed history → rotate (relocation is theater).
- Scrubbing (filter-repo/BFG) is secondary — rotation first.
D · Deployment env config (Vercel / AWS) [HIGH freedom]
- Secrets in platform env store, not baked into build.
.env.example not committed with real values.
- No secrets as build args (persist in image layers).
E · .gitignore & hygiene [HIGH freedom]
.env* ignored; no secrets in README, comments, test fixtures.
Procedure [HIGH freedom]
- Scan working tree (A), classify scope (B).
- Check history for every credential (C).
- Review deploy + hygiene (D, E).
- Score. Never-client in client bundle or history = Critical.
- Phase. Emit
plan-secrets-audit.md. End the turn.
Guardrails [LOW freedom — run exactly]
- Plan only. No rotation, history rewriting, or env edits.
- Never print the secret. Type + location + last 4 chars at most.
- Rotate beats relocate — always say which.
- Don't assume safe-client keys are fine. Hand anon-key + no-RLS to
plan-rls-audit.
- Order: rotate → update env store → redeploy → (optional) scrub history.
Self-critique before the burndown [LOW freedom — do not skip]
- evidenced-not-assumed — type + location + last 4; never the secret value
- plan-only — no rotation, scrub, or env edit this pass
- phase justified — history → rotate is Phase 1, not "move to
.env"
- right-owner — anon-key + no-RLS →
plan-rls-audit; generic OWASP → plan-security-audit
- no-false-safety — relocate-without-rotate is theater; safe-client still needs its protector
Report template — plan-secrets-audit.md
# Secrets & Key-Scope Audit — <repo>
_Audit-only. No key is rotated, moved, or scrubbed until each phase is approved._
## Scope
- Scanned: working tree ☐ git history ☐ deploy env (Vercel/AWS) ☐
- Assumptions / not inspected: …
## Verdict
| Severity | Count | Worst case |
|----------|-------|-----------|
| Critical | n | never-client secret exposed / in history |
| High | n | committed .env values, public-prefix leak |
| Medium | n | shared-env keys, hygiene |
## Findings
| # | Key type | Location | Scope bucket | In history? | Action | Sev |
|---|----------|----------|--------------|-------------|--------|-----|
| S1 | Supabase service_role | lib/admin.ts:4 | never-client | YES | ROTATE now | Crit |
| S2 | Stripe secret sk_ | api/pay.ts:2 | never-client | YES | ROTATE now | Crit |
| S3 | Supabase anon | client.ts:6 | safe-client | n/a | OK if RLS holds → plan-rls-audit | — |
| S4 | DB URL | .env.example | never-client | YES | ROTATE + remove from example | High |
## Phased burndown
- **Phase 1 — Rotate exposed never-client secrets** → dashboard rotation + Vercel/AWS env update (grace window)
- **Phase 2 — Relocate clean secrets** → move to env store, fix NEXT_PUBLIC_ leaks
- **Phase 3 — Hygiene** → .gitignore, remove committed examples, CI secret scanner
- **Phase 4 — (optional) Scrub history** → filter-repo/BFG, after rotation
## Execution handoff
Approve a phase to run it. Cross-hand safe-client keys to `plan-rls-audit`.
Add a pre-commit secret scanner (`create-hook`) so this can't regress.
Chains with
- Security spine — credentials layer (this skill); cross-hand to
plan-rls-audit for anon-key safety.
create-hook — pre-commit secret-scanning hook as regression guard.
- Execution: provider dashboards, Vercel/AWS env,
audit-security.
- Verify: re-scan working tree + history; confirm rotated keys are dead.
Plan with a strong model; execute with composer-2.5-execution.mdc riding
along. Rotation is irreversible-ish — the plan says which keys; the rule
constrains how and in what order.
1---2name: plan-secrets-audit3description: Audit the working tree and git history for exposed credentials and mis-scoped keys, then a rotate-vs-relocate plan. Use when "hardcoded secrets", "did I commit a key", "secret scan", "is my .env safe", or "rotate keys". Do not fire for "RLS audit" or generic "security burndown".4license: MIT5---67# Secrets & Key-Scope Audit + Rotation Plan89**Degree of freedom: MIXED** — prefix scan is exact; rotate-vs-relocate10is judgment. Stay **plan-only**. Never print secret values.1112## This skill vs neighbors1314| Skill | Owns |15|---|---|16| **plan-secrets-audit** (this) | Rotate vs relocate leaked keys |17| `plan-security-audit` | OWASP umbrella (not the first secrets match) |18| `plan-rls-audit` | Table access control |19| `audit-env-parity` | Env/config drift |2021## How to reason (every plan item)22231. **Propose** — rotate, relocate, or hygiene — say which, never print the value242. **Risk** — who can use the key if it stays live (history counts)253. **Keep-working** — keys that are scoped correctly and not in history264. **Phase** — rotate → relocate → hygiene → optional scrub (do not execute)2728## Worked example2930> **Propose:** rotate the Stripe secret at `api/pay.ts` (last-4 only in the report); history presence means rotate, not `.env` move.31> **Risk:** `sk_` in committed history stays valid forever after a relocate.32> **Keep-working:** Stripe `pk_` in the client is publishable by design (still note the protector).33> **Phase:** Phase 1 — rotate exposed never-client secrets.34> **Redaction:** type + path + last 4 — never the secret.3536**Role:** Senior security engineer (credential exposure + key scoping).3738**Task:** Scan working tree and git history, classify each credential (rotate vs39relocate), score by scope and permanence, emit `plan-secrets-audit.md`. **Audit & plan40only — no rotation, scrubbing, or env edits until each phase is approved.**4142**Find every leaked key. Decide rotate vs relocate. Change nothing until approved.**4344A widely reported breach started with a hardcoded Supabase key in client JavaScript — combined45with RLS off, the public key became an admin backdoor. **A secret committed even once46lives in git history forever.** Moving it to `.env` later does nothing; the only real47fix is **rotation**.4849---5051## When this fires5253Trigger phrases: *"scan for secrets"*, *"are my keys exposed"*, *"did I commit an54API key"*, *"is my .env safe"*, *"rotate keys"*, *"about to open-source this"*,55*"pre-launch secret check"*.5657Do **not** fire for: RLS policy correctness (`plan-rls-audit`), input/webhook58validation (`plan-input-validation`). This skill owns *credential exposure and59key scoping* specifically.6061---6263## Why a dedicated skill6465A grep finds strings. This skill adds the two judgments a grep can't: **scope**66(is this key *supposed* to be client-side?) and **permanence** (is it in history,67making relocation insufficient?).6869---7071## The audit7273### A · Pattern scan (working tree) [LOW freedom — run exactly]74Search outside `.env*` and server-only contexts for:75- Prefixes: `sk_`, `pk_`, `whsec_`, `service_role`, `eyJ...`, `AKIA`, `API_KEY`,76 `SECRET`, `TOKEN`, long random blobs.77- Supabase: anon vs `service_role`.78- Any key in files that ship to the browser.7980### B · Scope classification [HIGH freedom]81- **Safe client-side:** Supabase **anon**, Stripe **publishable** (`pk_`), public82 analytics keys — note the protector dependency (RLS, Stripe design).83- **Never client-side (Critical if exposed):** **service_role**, Stripe **secret**84 (`sk_`), **webhook secret** (`whsec_`), AWS secrets, DB URLs.85- **`NEXT_PUBLIC_` / `VITE_` / `EXPO_PUBLIC_` trap** — bundled into client.8687### C · Git history (rotate vs relocate) [HIGH freedom]88- Ever in committed history → **rotate** (relocation is theater).89- Scrubbing (filter-repo/BFG) is secondary — rotation first.9091### D · Deployment env config (Vercel / AWS) [HIGH freedom]92- Secrets in platform env store, not baked into build.93- `.env.example` not committed with real values.94- No secrets as build args (persist in image layers).9596### E · `.gitignore` & hygiene [HIGH freedom]97- `.env*` ignored; no secrets in README, comments, test fixtures.9899---100101## Procedure [HIGH freedom]1021031. **Scan** working tree (A), classify scope (B).1042. **Check history** for every credential (C).1053. **Review deploy + hygiene** (D, E).1064. **Score.** Never-client in client bundle or history = Critical.1075. **Phase.** **Emit `plan-secrets-audit.md`. End the turn.**108109---110111## Guardrails [LOW freedom — run exactly]112113- **Plan only.** No rotation, history rewriting, or env edits.114- **Never print the secret.** Type + location + last 4 chars at most.115- **Rotate beats relocate — always say which.**116- **Don't assume safe-client keys are fine.** Hand anon-key + no-RLS to117 `plan-rls-audit`.118- **Order:** rotate → update env store → redeploy → (optional) scrub history.119120## Self-critique before the burndown [LOW freedom — do not skip]1211221. **evidenced-not-assumed** — type + location + last 4; never the secret value1232. **plan-only** — no rotation, scrub, or env edit this pass1243. **phase justified** — history → rotate is Phase 1, not "move to `.env`"1254. **right-owner** — anon-key + no-RLS → `plan-rls-audit`; generic OWASP → `plan-security-audit`1265. **no-false-safety** — relocate-without-rotate is theater; safe-client still needs its protector127128---129130## Report template — `plan-secrets-audit.md`131132```markdown133# Secrets & Key-Scope Audit — <repo>134135_Audit-only. No key is rotated, moved, or scrubbed until each phase is approved._136137## Scope138- Scanned: working tree ☐ git history ☐ deploy env (Vercel/AWS) ☐139- Assumptions / not inspected: …140141## Verdict142| Severity | Count | Worst case |143|----------|-------|-----------|144| Critical | n | never-client secret exposed / in history |145| High | n | committed .env values, public-prefix leak |146| Medium | n | shared-env keys, hygiene |147148## Findings149| # | Key type | Location | Scope bucket | In history? | Action | Sev |150|---|----------|----------|--------------|-------------|--------|-----|151| S1 | Supabase service_role | lib/admin.ts:4 | never-client | YES | ROTATE now | Crit |152| S2 | Stripe secret sk_ | api/pay.ts:2 | never-client | YES | ROTATE now | Crit |153| S3 | Supabase anon | client.ts:6 | safe-client | n/a | OK if RLS holds → plan-rls-audit | — |154| S4 | DB URL | .env.example | never-client | YES | ROTATE + remove from example | High |155156## Phased burndown157- **Phase 1 — Rotate exposed never-client secrets** → dashboard rotation + Vercel/AWS env update (grace window)158- **Phase 2 — Relocate clean secrets** → move to env store, fix NEXT_PUBLIC_ leaks159- **Phase 3 — Hygiene** → .gitignore, remove committed examples, CI secret scanner160- **Phase 4 — (optional) Scrub history** → filter-repo/BFG, after rotation161162## Execution handoff163Approve a phase to run it. Cross-hand safe-client keys to `plan-rls-audit`.164Add a pre-commit secret scanner (`create-hook`) so this can't regress.165```166167---168169## Chains with170171- **Security spine** — credentials layer (**this skill**); cross-hand to172 `plan-rls-audit` for anon-key safety.173- **`create-hook`** — pre-commit secret-scanning hook as regression guard.174- **Execution:** provider dashboards, Vercel/AWS env, `audit-security`.175- **Verify:** re-scan working tree + history; confirm rotated keys are dead.176177> Plan with a strong model; execute with `composer-2.5-execution.mdc` riding178> along. Rotation is irreversible-ish — the plan says *which* keys; the rule179> constrains *how* and *in what order*.