Was a loose secret-hygiene.md at the root of ~/.claude/skills — not dir/SKILL.md, so no harness ever loaded it. Promoted to a real skill so it installs to Claude Code, Crush, Antigravity and Vibe alike. Frontmatter description carries load-before triggers (kubectl exec/env, whole-file SOPS decrypt, token rotation) so it fires ahead of the command that would leak, not after. Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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 <pod> -- env` (or `printenv`) | Renders every envFrom + valueFrom.secretKeyRef plaintext | `kubectl describe pod <pod>` — k8s redacts secretKeyRef in describe |
|
||||
| `sops -d <file>` to inspect | Whole-file decrypt dumps everything | `sops --extract '["key"]' <file>` 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/<unique>` 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).
|
||||
Reference in New Issue
Block a user