allod secret: land an encrypted credential from the host #195

Merged
vnprc merged 1 commit from agent/secret-namespace into master 2026-09-11 01:59:04 +01:00
Member

This adds allod secret create <name> and allod secret rekey <name>, the host-side command that lands an encrypted credential in the secrets repository, compiled in only where the build sets the new secret tag. On every machine built today nothing changes: no build sets the tag yet, so allod secret stays an unknown word, and the only shared code that moved is the hidden-prompt terminal helper, now in an untagged file with identical behavior. What proves it: the flake check passes with all four tag combinations, the tagged binary was smoke-tested against a real secrets checkout with real nix eval and a real age call, and the untagged test asserts the namespace is absent. If it is wrong, revert this commit; the nexus change that sets the tag is a separate PR and has not landed.

Refs allod/tools#182. Design decisions are recorded on the issue. The pending inventory state this depends on is allod/secrets#19; the tag is set by the nexus PR that closes the issue.

What create does

On a non-default branch with a clean tree, it requires the credentials.nix entry for <name> to be pending with exactly one agenix consumer here, the secrets.nix line for that path to list recipients including this host's identity, a rotation registry entry naming both, and no ciphertext yet. It reads the value from stdin verbatim or one hidden terminal line, refuses an empty one, pipes it into age -e with the recipients evaluated out of secrets.nix, writes the ciphertext whole, flips the entry to active in place, re-evaluates to confirm, runs nix flake check, restores both files if that fails, and otherwise commits the two files and pushes the branch. rekey decrypts with the host identity and re-encrypts to the current recipients, always rewriting. Nothing unencrypted is ever written to a filesystem.

Risk

R3. The command mutates the authoritative secrets repository and pushes, and handles the age identity. It is bounded to a non-default branch with a clean tree, every file effect is computed in memory before the first write, a failed check restores the tree, and merging remains a human act at the forge. The worst credible failure after validation is a ciphertext encrypted to the recipients secrets.nix declared on a branch that was wrong about them, which the reviewed diff and the inventory check see before the command does.

Validation

  • nix flake check on this branch: all checks passed, run as git+file://<worktree>. Run as path:<worktree> the parity suite fails on a test I did not touch, because path: copies a linked worktree's .git pointer file into the source and the one parity test that runs git without -C then sees a dangling repository; master's path: run passes because its .git is a directory nix drops. That is a testing gotcha, not a defect in this change, and will go into memory.
  • Go tests for -tags secret and -tags site,secret cover the landing, the terminal input path, fourteen create refusals, restoration on a failed check, a failed push, rekey and its refusals, checkout resolution, and the text flip. The untagged and -tags site runs assert the namespace absent.
  • Smoke test of the tagged binary against a clone of the secrets template with real nix eval: an active credential is refused as not pending, a .pub that is not a recipient is refused, the default branch and a dirty tree are refused, an unknown name is refused, and rekey reaches a real age -d call, which fails on the fixture identity as expected. A full landing was not run: this VM holds no age identity and no pending entry exists yet.
  • A real age -e | age -d round trip through a generated key confirmed the flags the helpers use.
This adds `allod secret create <name>` and `allod secret rekey <name>`, the host-side command that lands an encrypted credential in the secrets repository, compiled in only where the build sets the new `secret` tag. On every machine built today nothing changes: no build sets the tag yet, so `allod secret` stays an unknown word, and the only shared code that moved is the hidden-prompt terminal helper, now in an untagged file with identical behavior. What proves it: the flake check passes with all four tag combinations, the tagged binary was smoke-tested against a real secrets checkout with real `nix eval` and a real `age` call, and the untagged test asserts the namespace is absent. If it is wrong, revert this commit; the nexus change that sets the tag is a separate PR and has not landed. Refs allod/tools#182. Design decisions are recorded on the issue. The `pending` inventory state this depends on is allod/secrets#19; the tag is set by the nexus PR that closes the issue. ## What create does On a non-default branch with a clean tree, it requires the `credentials.nix` entry for `<name>` to be `pending` with exactly one agenix consumer here, the `secrets.nix` line for that path to list recipients including this host's identity, a rotation registry entry naming both, and no ciphertext yet. It reads the value from stdin verbatim or one hidden terminal line, refuses an empty one, pipes it into `age -e` with the recipients evaluated out of `secrets.nix`, writes the ciphertext whole, flips the entry to `active` in place, re-evaluates to confirm, runs `nix flake check`, restores both files if that fails, and otherwise commits the two files and pushes the branch. `rekey` decrypts with the host identity and re-encrypts to the current recipients, always rewriting. Nothing unencrypted is ever written to a filesystem. ## Risk R3. The command mutates the authoritative secrets repository and pushes, and handles the age identity. It is bounded to a non-default branch with a clean tree, every file effect is computed in memory before the first write, a failed check restores the tree, and merging remains a human act at the forge. The worst credible failure after validation is a ciphertext encrypted to the recipients `secrets.nix` declared on a branch that was wrong about them, which the reviewed diff and the inventory check see before the command does. ## Validation - `nix flake check` on this branch: all checks passed, run as `git+file://<worktree>`. Run as `path:<worktree>` the parity suite fails on a test I did not touch, because `path:` copies a linked worktree's `.git` pointer file into the source and the one parity test that runs git without `-C` then sees a dangling repository; master's `path:` run passes because its `.git` is a directory nix drops. That is a testing gotcha, not a defect in this change, and will go into memory. - Go tests for `-tags secret` and `-tags site,secret` cover the landing, the terminal input path, fourteen create refusals, restoration on a failed check, a failed push, rekey and its refusals, checkout resolution, and the text flip. The untagged and `-tags site` runs assert the namespace absent. - Smoke test of the tagged binary against a clone of the secrets template with real `nix eval`: an active credential is refused as not pending, a `.pub` that is not a recipient is refused, the default branch and a dirty tree are refused, an unknown name is refused, and `rekey` reaches a real `age -d` call, which fails on the fixture identity as expected. A full landing was not run: this VM holds no age identity and no pending entry exists yet. - A real `age -e | age -d` round trip through a generated key confirmed the flags the helpers use.
vnprc approved these changes 2026-09-11 01:58:59 +01:00
vnprc merged commit 19972c4469 into master 2026-09-11 01:59:04 +01:00
vnprc deleted branch agent/secret-namespace 2026-09-11 01:59:05 +01:00
Sign in to join this conversation.
No description provided.