Derive declared credentials' recipients from the rotation registry #47

Merged
vnprc merged 1 commit from agent/derive-registry-recipients into master 2026-10-18 23:48:52 +01:00
Member

secrets.nix now computes the recipients of every credential listed in rotation-registry.json, the way it already computes them for forge keys, VM host keys and Pi credentials, so a declared credential needs no hand-written recipient line. On the template nothing changes: its registry is empty, and the evaluated secrets.nix is byte for byte what it was. What proves it: the new registry-recipients check pins the derivation on a synthetic registry, refuses a target machine missing from the key file, two credentials on one path and a bad hypervisor key list, and asserts secrets.nix carries every derived line unchanged; the whole gate passes. If it is wrong, revert the one commit; nothing consumes the new map until a fork adopts it and the tool stops writing its line, both tracked separately.

This is the first of three steps in allod/secrets#46. The second is each deployment fork adopting the derivation in its own secrets repo; the third is allod secret declare dropping its secrets.nix insertion, filed on allod/tools once this merges and reaching hosts only after the fork step.

What changed

  • lib/registry-recipients.nix returns { recipients; diagnostics; }. recipients maps each registry credential's secret_path to hypervisorPublicKeys followed by the active and staged keys of each target whose kind is not nixos-host, in target order, each machine once; it throws with every diagnostic when there is one. diagnostics is the list itself, so a check can assert the wording: a target machine absent from machine-host-keys.json names the credential and the machine and says a hypervisor target needs kind "nixos-host"; two credentials on one secret_path name the path; a hypervisor key list that is empty, non-unique or not strings is named. Builtins only, because agenix evaluates secrets.nix standalone.
  • secrets.nix reads rotation-registry.json and merges the derived map last.
  • flake.nix exports lib.registryRecipients, built from the validated registry, and hands it to the checks.
  • checks/registry-recipients.nix: an empty registry derives nothing; the fixture of two groups and three credentials, one host-only and one with a repeated target, derives the expected lists with no diagnostics; four sabotage fixtures are refused, and the two that name things are checked for their exact message; every path in the flake's map is present in secrets.nix with equal recipients. That last assertion is vacuous on the template, whose registry is empty, and bites in a fork; the README row says so.
  • README: the lib row, the check row, and one bullet under the recipient model.

Risk

R2. A new derivation merged into the recipient map of a security-relevant file, localised and fixture-tested, with the template's output proven identical. Rollback is a revert. The fork step and the tool step carry their own scores.

Validation

  • nix build .#checks.x86_64-linux.registry-recipients builds in the worktree.
  • nix eval --json --file secrets.nix in the shared checkout and in the worktree, compared with cmp: identical.
  • allod check in the worktree: all 10 steps passed, 9 checks built, the new one among them.
  • Cold cross-family review, one seat as R2 buys: gpt-5.6-sol through pi's nullsink provider, since codex holds no credentials on this VM. Eight findings, all verified against the source and all taken in one fix round: a diagnostic that interpolated an unvalidated credential name (now guarded); a target without kind being looked up as a guest (kept as a refusal, now naming the remedy); the staged key read tolerating an absent field (now strict, matching the existing vmKeys helper); sabotage fixtures that only proved some throw happened (the lib now exposes diagnostics and the check asserts the exact messages); the secrets.nix consumption assertion being vacuous on the template (true, stated in the README row); and three comments that explained or narrated (cut to one line each or deleted). The three witnesses above were re-run on the final code.
  • Not run: a fork with a populated registry. The check's fixture is that shape; the fork step of allod/secrets#46 is where the live witness happens.

Refs allod/secrets#46

`secrets.nix` now computes the recipients of every credential listed in `rotation-registry.json`, the way it already computes them for forge keys, VM host keys and Pi credentials, so a declared credential needs no hand-written recipient line. On the template nothing changes: its registry is empty, and the evaluated `secrets.nix` is byte for byte what it was. What proves it: the new `registry-recipients` check pins the derivation on a synthetic registry, refuses a target machine missing from the key file, two credentials on one path and a bad hypervisor key list, and asserts `secrets.nix` carries every derived line unchanged; the whole gate passes. If it is wrong, revert the one commit; nothing consumes the new map until a fork adopts it and the tool stops writing its line, both tracked separately. This is the first of three steps in allod/secrets#46. The second is each deployment fork adopting the derivation in its own secrets repo; the third is `allod secret declare` dropping its `secrets.nix` insertion, filed on allod/tools once this merges and reaching hosts only after the fork step. ## What changed - `lib/registry-recipients.nix` returns `{ recipients; diagnostics; }`. `recipients` maps each registry credential's `secret_path` to `hypervisorPublicKeys` followed by the active and staged keys of each target whose `kind` is not `nixos-host`, in target order, each machine once; it throws with every diagnostic when there is one. `diagnostics` is the list itself, so a check can assert the wording: a target machine absent from `machine-host-keys.json` names the credential and the machine and says a hypervisor target needs `kind "nixos-host"`; two credentials on one `secret_path` name the path; a hypervisor key list that is empty, non-unique or not strings is named. Builtins only, because agenix evaluates `secrets.nix` standalone. - `secrets.nix` reads `rotation-registry.json` and merges the derived map last. - `flake.nix` exports `lib.registryRecipients`, built from the validated registry, and hands it to the checks. - `checks/registry-recipients.nix`: an empty registry derives nothing; the fixture of two groups and three credentials, one host-only and one with a repeated target, derives the expected lists with no diagnostics; four sabotage fixtures are refused, and the two that name things are checked for their exact message; every path in the flake's map is present in `secrets.nix` with equal recipients. That last assertion is vacuous on the template, whose registry is empty, and bites in a fork; the README row says so. - README: the `lib` row, the check row, and one bullet under the recipient model. ## Risk R2. A new derivation merged into the recipient map of a security-relevant file, localised and fixture-tested, with the template's output proven identical. Rollback is a revert. The fork step and the tool step carry their own scores. ## Validation - `nix build .#checks.x86_64-linux.registry-recipients` builds in the worktree. - `nix eval --json --file secrets.nix` in the shared checkout and in the worktree, compared with `cmp`: identical. - `allod check` in the worktree: all 10 steps passed, 9 checks built, the new one among them. - Cold cross-family review, one seat as R2 buys: `gpt-5.6-sol` through pi's `nullsink` provider, since codex holds no credentials on this VM. Eight findings, all verified against the source and all taken in one fix round: a diagnostic that interpolated an unvalidated credential name (now guarded); a target without `kind` being looked up as a guest (kept as a refusal, now naming the remedy); the staged key read tolerating an absent field (now strict, matching the existing `vmKeys` helper); sabotage fixtures that only proved some throw happened (the lib now exposes `diagnostics` and the check asserts the exact messages); the `secrets.nix` consumption assertion being vacuous on the template (true, stated in the README row); and three comments that explained or narrated (cut to one line each or deleted). The three witnesses above were re-run on the final code. - Not run: a fork with a populated registry. The check's fixture is that shape; the fork step of allod/secrets#46 is where the live witness happens. Refs allod/secrets#46
secrets.nix computes the recipients of every credential in rotation-registry.json from its targets and machine-host-keys.json, the way it already derives forge keys, VM host keys and Pi credentials, so a declared credential needs no hand-written recipient line. The template registry is empty and the evaluated secrets.nix is unchanged; a new check pins the derivation on a synthetic registry and asserts the refusals by message.

Refs allod/secrets#46
vnprc approved these changes 2026-10-18 23:48:45 +01:00
vnprc merged commit 8613a9306d into master 2026-10-18 23:48:52 +01:00
vnprc deleted branch agent/derive-registry-recipients 2026-10-18 23:48:52 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
allod/secrets!47
No description provided.