--- 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).