refactor(secrets): consumers read the store; drop materialized copies

Materializing wrote a second plaintext file per secret, which is the problem the store
was meant to solve: two files drift, and the copy is what ends up committed or grepped.

- tangled_pr / tangled_pr_edit / tangled_repo now read tangled.cookie from the store.
  engine/.tangled-session is deleted; --cookie-file remains as a legacy escape hatch.
- materialize() is replaced by run-time injection that leaves nothing at rest:
    exec-env <group> -- cmd      group as env vars (use this instead of a systemd
                                 EnvironmentFile — same effect, no plaintext on disk)
    with-file <key> -- cmd {}    0600 file in a private tmpdir, removed when cmd exits,
                                 for consumers that insist on a path (ssh -i, a TLS key)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-01 16:58:26 +00:00
co-authored by Claude
parent 6c56c1953e
commit 300e69d3b3
5 changed files with 110 additions and 67 deletions
+17 -4
View File
@@ -385,10 +385,23 @@ python3 engine/secrets.py materialize tangled-session # write a runtime file f
sops /secrets/store.yaml # add/edit: decrypts to $EDITOR, re-encrypts on save
```
**Materialized files.** Some consumers read a fixed path and can't be taught otherwise (a systemd
`EnvironmentFile`, an ssh `IdentityFile`, nix's `authKeyFile`). Those files still exist at 0600,
but **the store is the source of truth**`materialize` rewrites them from it. Never hand-edit a
materialized file: edit the store and re-materialize, or the two silently drift.
**No second copies.** A secret must never be written to a second file "so something can read
it" — copies drift from the store, get committed, and widen what a stray `grep` or an attacker
finds. Our own code imports this module. Anything else gets the value at **run time**:
```sh
# a group as environment variables — nothing touches the disk
python3 engine/secrets.py exec-env cc_ci_testenv -- some-command
# a consumer that insists on a path: 0600 file in a private tmpdir, deleted when the command exits
python3 engine/secrets.py with-file ssh_keys.tangled-ed25519 -- ssh -i {} host
```
For **systemd**, wrap `ExecStart` in `exec-env` rather than using an `EnvironmentFile`: same
effect, no plaintext at rest. The genuine exceptions are OS-level paths that are read before any
of this exists — nix's `authKeyFile`, sshd host keys, and ssh client keys used by bare `git push`.
Those stay where the OS expects them; do not also copy them into the store, or you have two
sources of truth again.
**Rules of thumb**