fix: decrypt SOPS through explicit custody into private output
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
This commit is contained in:
tegwick 2026-10-12 12:08:35 +02:00
parent 4cf26a9bc8
commit 681c5b0ea1
6 changed files with 499 additions and 27 deletions

View file

@ -174,8 +174,53 @@ its trap while a foreground child runs; this is not a process-group supervisor,
SIGKILL cleanup guarantee or a runtime authority boundary. Temporary key cleanup
on command return does not certify that an arbitrary child retained no copies.
Remaining concrete caller defects stay in NK-WP-0049-T02: Make `sops-setup` /
`creds-init` still install or require permanent HOME keys; Make `sops-decrypt`
still emits plaintext; legacy rotation and privacyIDEA bootstrap helpers contain
secret-bearing output. Do not use those legacy paths as the recovery procedure
Remaining concrete caller defects stay in NK-WP-0049-T02: legacy rotation and
privacyIDEA bootstrap helpers contain secret-bearing output; direct editor,
rotate and delegated apply/init paths still need their own custody/output audit. Do not use those legacy paths as the recovery procedure
or treat these narrower helper repairs as closure of the whole custody surface.
## Make recovery entry points — 2026-10-12
`make sops-setup` now validates only `keys/age.pub`. It never copies or generates
a private key; `creds-init` likewise has no permanent HOME key requirement.
Existing permanent files are not deleted or represented as remediated copies.
`make sops-decrypt FILE=... OUTPUT=...` requires the custody helper's explicit
`NETKINGDOM_CUSTODY_AGE_KEY_FILE`. The owned private regular key file must match
the registered public recipient. Output must be a new file in an owned private
parent outside Git worktrees (including linked-worktree markers and bare repos).
No destination file/symlink is overwritten. Partial tool output is removed on
failure; successful publication is atomic and mode 0600. The operator removes
successful plaintext after the approved operation. This does not promise cleanup
after SIGKILL, hostile same-UID interference or arbitrary child copies.
For the repository's age custody profile, the wrapper accepts SOPS JSON/YAML,
and rejects non-age recipients, age plugins, key groups, unknown metadata and
duplicate keys before decryption.
Other formats/backends require their own reviewed adapter; they are not silently
sent through a different credential source. YAML input requires PyYAML.
SOPS runs with an isolated HOME/XDG configuration and a minimal environment,
using an inherited descriptor for the explicit key. This prevents SOPS from
combining it with HOME/SSH keys, inline keys, key commands, proxies or remote key
services. Raw parser/tool diagnostics and plaintext stdout never enter the
terminal transcript. This wrapper assumes trusted installed `age`, `age-keygen`
and `sops` binaries; it is not an OS sandbox or task-authorization grant.
Use from the operator's approved custody terminal, with a private directory
created outside all repositories (for example using `mktemp -d`):
```bash
# OUTPUT's parent must exist and be owned/private; select a new filename.
make sops-custody-run COMMAND='make sops-decrypt FILE=secrets/example.yaml OUTPUT=/private/recovery/new-example.yaml'
```
The command string carries paths only. Never put a credential in FILE, OUTPUT,
COMMAND, shell history or Make variables. Tests exercise real SOPS 3.13.3 with
disposable keys; no production key or encrypted secret was accessed. The test
binary checksum was checked against the official release checksum; no detached
signature verification or installed-toolchain promotion is claimed.
Source basis: [SOPS age identity loading](https://github.com/getsops/sops/blob/v3.13.3/age/keysource.go)
loads explicit and ambient identities together, so setting `SOPS_AGE_KEY_FILE`
alone is insufficient to exclude ambient credentials.