diff --git a/SKILLS_INDEX.md b/SKILLS_INDEX.md index 568b0d5..2d1d204 100644 --- a/SKILLS_INDEX.md +++ b/SKILLS_INDEX.md @@ -25,6 +25,7 @@ This index lists all available engineering skills. Load the full SKILL.md on dem | `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | | `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists | | `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry | +| `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak | | `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data | | `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | | `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | diff --git a/secret-hygiene/SKILL.md b/secret-hygiene/SKILL.md new file mode 100644 index 0000000..d5849b8 --- /dev/null +++ b/secret-hygiene/SKILL.md @@ -0,0 +1,76 @@ +--- +name: secret-hygiene +description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong". +--- + +# Secret hygiene for agent sessions + +Goal: stop the recurring leak pattern where a diagnostic command renders a +SOPS-encrypted value or a Kubernetes Secret value into the session +transcript, forcing a rotation. + +**Confirmed past leaks** (see `mathias/infra#39` rotation umbrella): +- `POSTGRES_PASSWORD` — `kubectl exec ... env` rendered envFrom value +- `INFRA_MCP_TOKEN` — `sops -d ingress.enc.yaml` rendered the bearer in the configuration-snippet +- `brain_app` postgres DSN — pre-fix ingestion pod log (hyperguild#20) + +Three are pattern, not bug. + +## Rules — never do these + +| Forbidden | Why | Use instead | +|---|---|---| +| `kubectl exec -- env` (or `printenv`) | Renders every envFrom + valueFrom.secretKeyRef plaintext | `kubectl describe pod ` — k8s redacts secretKeyRef in describe | +| `sops -d ` to inspect | Whole-file decrypt dumps everything | `sops --extract '["key"]' ` for pinpoint reads | +| `kubectl get secret ... -o jsonpath='{.data}'` (whole map) | Returns base64 of every key; trivial to b64-decode the lot | `... -o jsonpath='{.data.SPECIFIC_KEY}'` for the one key needed | +| Echoing a generated token to verify length / content | Renders the new token into transcript, defeating the rotation | Verify functionally: `curl ... -H "Authorization: Bearer $(cat /tmp/tokfile)"` — never echo `$NEW_TOK` | +| Storing a token in a shell variable then running `echo` / `set` / `env` | Same as above | Pipe through file (mode 600) or stdin to the next consumer | + +## Rules — do these instead + +- **Verify by side-effect, not by value.** If a token rotation should make + the old token return 401 and the new one return 200, test those two + HTTP responses. Never check `$OLD == $LEAKED` by printing it. +- **Pinpoint SOPS reads.** When you need ONE key from a SOPS file, always + `sops --extract '["..."]'`. The shape of the key path is safe to log; + the value isn't. +- **Stash tokens in `/dev/shm/` mode 600** and pass via filename, + not env. `shred -u` after use. Avoid `/tmp` if the system has `/dev/shm`. +- **If you must dump a secret to confirm wiring**, immediately rotate it + in the same commit train. Treat the dump as "this is now compromised". + +## Rotation chain pattern when a token is already exposed + +If a token was leaked in a prior step of the same session, chain two +rotations: + +1. **T1**: replace the leaked value with a new one. The verification step + may also leak T1 (acceptable risk). +2. **T2**: replace T1 with a fresh value. Run this without any decrypt + step that emits to stdout. The new value lives only in the encrypted + file + the consumer's secret store (`~/.llmkeys`, the k8s Secret). + +Example T2 path (Python, never decrypts to stdout): + +```python +new_t2 = subprocess.check_output(["openssl","rand","-hex","32"]).decode().strip() +plaintext = subprocess.check_output(["sops","-d", path]).decode() # in-memory only +patched = re.sub(r'Bearer [a-f0-9]+(?=")', f"Bearer {new_t2}", plaintext) +# write patched via /dev/shm tmp + sops -e → write back +# DO NOT print(patched), DO NOT print(new_t2) +``` + +After T2 lands via GitOps + Flux, update consumers from the file stash +without echoing. + +## Detection ideas (not blocking — telemetry) + +- Add a `bash` pre-hook in Claude harness that refuses obvious patterns + (`kubectl exec * env*`, `sops -d *.enc.yaml`). Filed as a follow-up. +- Run a periodic grep over `~/.claude/projects/*/sessions/*.jsonl` for + high-entropy strings matching `[a-f0-9]{32,}` to catch new leak surface. + +## Reference + +- `gitea.d-ma.be/mathias/infra#39` — umbrella credential rotation issue with confirmed-leak roster. +- `gitea.d-ma.be/mathias/infra#44` — POSTGRES_PASSWORD rotation procedure (template for future incidents).