Derive declared credentials' recipients from the rotation registry so declare writes no secrets.nix line #46

Closed
opened 2026-10-18 23:01:23 +01:00 by allod-agent · 1 comment
Member

secrets.nix computes the recipients of every declared credential from rotation-registry.json and machine-host-keys.json, the way it already computes them for forge keys, VM host keys and Pi credentials, so allod secret declare stops appending a hand-written recipient line and a deployment fork's own name for its hypervisor keys never has to match the tool's. Half of this is a handoff: the public template and the tool change here, and each deployment fork adopts the derivation in its own secrets repo.

Primary goals:

  • One derivation for declared credentials — a lib/registry-recipients.nix beside lib/pi-credential-recipients.nix, parameterised the same way over the registry, machineHostKeys and an ordered hypervisorPublicKeys list, maps each group's credentials[].secret_path to the hypervisor keys plus vmKeys of every target whose kind is not nixos-host. secrets.nix merges its result like the other four maps.
  • Template output unchanged — the template registry is empty, so the merged attrset is identical; a fixture registry under checks/ proves the mapping for a VM target, a host-only target and a shared group, and a sabotage fixture whose target names a machine absent from machine-host-keys.json fails naming the credential and the machine.
  • declare writes two files, not three — the allod/tools half drops the secrets.nix insertion, its collision check and the balanced-brace check on that file, and updates its help, docs/allod-secret.md and the public memory's age.md paragraph that lists three insertions. create keeps reading recipients by evaluating secrets.nix for the path, so it sees the derived set with no change.
  • Each fork does its half — a fork copies the lib file, replaces its hand-written declared lines with the derived map, passes its hypervisor keys as the list parameter (an active and a staged key both fit; nothing forces a rename), and witnesses equality: every publicKeys list evaluates to the same keys per path before and after, then the credential-inventory check passes.
  • Order holds — the tool stops writing the line only after every fork that uses declare derives. The criterion a reader can check: that fork's secrets.nix has no top-level "secrets/<name>.age".publicKeys line left. Breaking the order fails loudly rather than badly: create refuses with secrets.nix in <checkout> does not declare '<path>', and no ciphertext is written to the wrong recipients.

Current state and specifics

The template's secrets.nix is already fully derived: agentTokenRecipients, forgeKeyRecipients, vmHostKeyRecipients and piCredentialRecipients are merged and no top-level "secrets/<name>.age".publicKeys line remains. declare needs one to insert after and refuses the template today with secrets.nix: no existing publicKeys line to insert after (allod/tools cmd/allod/secret_declare.go, declareInsertSecretsLine); in a fork that has such lines it writes the template's literal [ hostKey ], which a fork binding its keys under another name cannot evaluate (allod/tools#271, closed in favour of this issue; allod/tools#275, a parser for the fork's name, closed unmerged because the line it parses should not exist).

The registry carries what the derivation needs. Each group lists credentials[].secret_path and credentials[].targets[].system and .kind; declare derives kind from the inventory's machine type and writes nixos-host for the hypervisor, which contributes no vmKeys call because the hypervisor keys already cover it. The host keys per VM are machineHostKeys.<system>.active and .staged, which vmKeys in secrets.nix already resolves.

checks/credential-inventory.nix resolves every publicKeys list secrets.nix produces (map (s: s.publicKeys) (builtins.attrValues secretsNix)), so an unresolved key in the derived map already fails there; the sabotage fixture above is for the missing-machine case, which must name what is missing rather than fail on an attribute lookup.

The public memory's age.md states the rule this completes under "secrets.nix owns the recipient list": a hand-written recipient list is a second source of truth that drifts silently. The declared-credential line was the last hand-written one.

Fork half

A deployment fork whose secrets.nix carries hand-written declared lines does, in its own repo: copy lib/registry-recipients.nix; add the derived map to its merge; delete each hand-written line the map now produces; pass its hypervisor key list where the template passes identity.hostPublicKeys. The witness is derivation equality on the recipient sets: evaluate builtins.mapAttrs (_: s: s.publicKeys) of secrets.nix before and after and diff the two, then run the repo's checks. Its receipts live in the fork's own tracker; this issue records only that the step exists and gates the tool change.

Scope

In, this repository: the lib file, the secrets.nix map, the fixtures and sabotage check, and the README lib row. Tracked elsewhere: the allod/tools change, filed as its own issue when this one merges and landing on hosts by lock bump only once the order criterion above holds; each fork's adoption, in that fork's tracker; the shape of the hypervisor key binding inside a fork, which stays the fork's choice.

`secrets.nix` computes the recipients of every declared credential from `rotation-registry.json` and `machine-host-keys.json`, the way it already computes them for forge keys, VM host keys and Pi credentials, so `allod secret declare` stops appending a hand-written recipient line and a deployment fork's own name for its hypervisor keys never has to match the tool's. Half of this is a handoff: the public template and the tool change here, and each deployment fork adopts the derivation in its own secrets repo. Primary goals: - **One derivation for declared credentials** — a `lib/registry-recipients.nix` beside `lib/pi-credential-recipients.nix`, parameterised the same way over the registry, `machineHostKeys` and an ordered `hypervisorPublicKeys` list, maps each group's `credentials[].secret_path` to the hypervisor keys plus `vmKeys` of every target whose `kind` is not `nixos-host`. `secrets.nix` merges its result like the other four maps. - **Template output unchanged** — the template registry is empty, so the merged attrset is identical; a fixture registry under `checks/` proves the mapping for a VM target, a host-only target and a shared group, and a sabotage fixture whose target names a machine absent from `machine-host-keys.json` fails naming the credential and the machine. - **`declare` writes two files, not three** — the `allod/tools` half drops the `secrets.nix` insertion, its collision check and the balanced-brace check on that file, and updates its help, `docs/allod-secret.md` and the public memory's `age.md` paragraph that lists three insertions. `create` keeps reading recipients by evaluating `secrets.nix` for the path, so it sees the derived set with no change. - **Each fork does its half** — a fork copies the lib file, replaces its hand-written declared lines with the derived map, passes its hypervisor keys as the list parameter (an active and a staged key both fit; nothing forces a rename), and witnesses equality: every `publicKeys` list evaluates to the same keys per path before and after, then the `credential-inventory` check passes. - **Order holds** — the tool stops writing the line only after every fork that uses `declare` derives. The criterion a reader can check: that fork's `secrets.nix` has no top-level `"secrets/<name>.age".publicKeys` line left. Breaking the order fails loudly rather than badly: `create` refuses with `secrets.nix in <checkout> does not declare '<path>'`, and no ciphertext is written to the wrong recipients. ### Current state and specifics The template's `secrets.nix` is already fully derived: `agentTokenRecipients`, `forgeKeyRecipients`, `vmHostKeyRecipients` and `piCredentialRecipients` are merged and no top-level `"secrets/<name>.age".publicKeys` line remains. `declare` needs one to insert after and refuses the template today with `secrets.nix: no existing publicKeys line to insert after` (`allod/tools` `cmd/allod/secret_declare.go`, `declareInsertSecretsLine`); in a fork that has such lines it writes the template's literal `[ hostKey ]`, which a fork binding its keys under another name cannot evaluate (allod/tools#271, closed in favour of this issue; allod/tools#275, a parser for the fork's name, closed unmerged because the line it parses should not exist). The registry carries what the derivation needs. Each group lists `credentials[].secret_path` and `credentials[].targets[].system` and `.kind`; `declare` derives `kind` from the inventory's machine type and writes `nixos-host` for the hypervisor, which contributes no `vmKeys` call because the hypervisor keys already cover it. The host keys per VM are `machineHostKeys.<system>.active` and `.staged`, which `vmKeys` in `secrets.nix` already resolves. `checks/credential-inventory.nix` resolves every `publicKeys` list `secrets.nix` produces (`map (s: s.publicKeys) (builtins.attrValues secretsNix)`), so an unresolved key in the derived map already fails there; the sabotage fixture above is for the missing-machine case, which must name what is missing rather than fail on an attribute lookup. The public memory's `age.md` states the rule this completes under "secrets.nix owns the recipient list": a hand-written recipient list is a second source of truth that drifts silently. The declared-credential line was the last hand-written one. ### Fork half A deployment fork whose `secrets.nix` carries hand-written declared lines does, in its own repo: copy `lib/registry-recipients.nix`; add the derived map to its merge; delete each hand-written line the map now produces; pass its hypervisor key list where the template passes `identity.hostPublicKeys`. The witness is derivation equality on the recipient sets: evaluate `builtins.mapAttrs (_: s: s.publicKeys)` of `secrets.nix` before and after and diff the two, then run the repo's checks. Its receipts live in the fork's own tracker; this issue records only that the step exists and gates the tool change. ### Scope In, this repository: the lib file, the `secrets.nix` map, the fixtures and sabotage check, and the README `lib` row. Tracked elsewhere: the `allod/tools` change, filed as its own issue when this one merges and landing on hosts by lock bump only once the order criterion above holds; each fork's adoption, in that fork's tracker; the shape of the hypervisor key binding inside a fork, which stays the fork's choice.
Author
Member

All three steps have landed. The derivation: allod/secrets#47. The fork adoption: done and merged in the deployment's private secrets repo, per the owner's report; this tracker cannot read it. The tool: allod/tools#279, with the memory paragraph in allod/memory#84. The order criterion held before the tool change merged. What remains is operational and outside this issue: the host picks up the new declare at the next deploy lock bump of allod-tools.

All three steps have landed. The derivation: allod/secrets#47. The fork adoption: done and merged in the deployment's private secrets repo, per the owner's report; this tracker cannot read it. The tool: allod/tools#279, with the memory paragraph in allod/memory#84. The order criterion held before the tool change merged. What remains is operational and outside this issue: the host picks up the new declare at the next deploy lock bump of allod-tools.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#46
No description provided.