Declare the host user's credential files from the hypervisor identity #75

Merged
vnprc merged 6 commits from agent/host-user-credentials into master 2026-09-10 16:44:43 +01:00
Member

The hypervisor builder now places two files in the human account's home from its identity: the account's own Forge API token at ~/.config/git/forgejo-token and the site hosting rclone config at ~/.config/rclone/rclone.conf, each an agenix-owned symlink, mode 0600, and each only when the identity names an age file for it. Nothing changes for any machine today: the template identity has neither field, the module reads them with or null, and the template hypervisor still generates zero age secrets against the current secrets pin. A new check builds the template hypervisor both ways and pins the exact secret set (file, path, owner, group, mode, symlink) plus the generated decrypt activation; it was shown to fail on a wrong mode and on symlink = false and pass again when reverted. If this is wrong, a deployment that has set the fields sees the difference only after its own secrets change plus a rebuild; reverting means dropping the one import line in mkHypervisor. A deployment that carries its own placement module for these paths deletes it after its pin advance, otherwise agenix declares the same secret twice.

Closes allod/archetypes#74. Refs allod/strategy#51, allod/secrets#17.

What changed

  • modules/host-user-credentials.nix: takes identity from specialArgs, declares age.secrets.user-forgejo-token and age.secrets.site-hosting-rclone from userForgejoTokenFile and siteHostingConfigFile, and refuses a string value with the same path-or-null contract mkDevVm enforces (allod/archetypes#70 may relax both together). The header states the agenix facts read from the pinned revision: installSecret runs mkdir -p on the custom path's parent as root, installs with ln -sfT so a hand-written file is replaced in place, and cleanup removes only the previous generation so a dropped declaration leaves a dangling symlink for the operator.
  • flake.nix: mkHypervisor imports the module beside netrc.nix; a host-user-credentials check, modelled on dev-forge-opt-out, asserts the null branch (neither secret, no activation text naming either), the fixture branch (literal projection and a grep of the generated agenix install script for both ciphertext basenames, after forcing the toplevel so module assertions run), and the string-form sabotage for each field with a path-form control.
  • checks/fixtures/host-user-credentials/*.age: two placeholder files whose Nix paths the fixture branch uses; the content is a comment, never decrypted.
  • README.md: one sentence under the credentials section.

The hypervisor builder composes no age.identityPaths; the consumer's own module owns that (nexus docs/integration.md, "Consumer-owned configuration"), so the check supplies one through the existing definition.nixosModules seam for the fixture only.

Risk

R1. The module is inert until an identity names a file. On a host whose first activation runs before the account exists, agenix creates /home/<user> and ~/.config root-owned ahead of the users step, and a root-owned ~/.config blocks the account from creating anything beneath it until chowned; the dev-machine token module has the same exposure on every fresh dev VM, and a real hypervisor already has the account. The check's fixture branch is the only place a hypervisor with these secrets has been evaluated; no deployment was rebuilt. Not run: fleet-diff against a deployment that sets both fields, since none is reachable from this public checkout.

Validation

  • nix build .#checks.x86_64-linux.host-user-credentials passes on the final commit.
  • Sabotage runs: changing one secret's mode to 0644 fails the check with with-access fixture no longer generates today's two secrets...; setting symlink = false on both fails the same way; each reverted and rebuilt green.
  • nix build .#checks.x86_64-linux.dev-forge-opt-out still passes.
  • nix eval --json .#nixosConfigurations.nexus.config.age.secrets is {} and the template hypervisor's system.build.toplevel.drvPath evaluates against the current secrets pin, which lacks the fields.
  • Reviewed by codex (gpt-5.6-sol) and pi (gpt-oss-120b) over four rounds. Applied: the projection now pins symlink; the header names which tool reads the user token, scopes the activation-order claim to the activation-script path, names ~/.config itself among the root-owned parents on a first activation, and separates the one-removed and both-removed cases. Rejected against source: the fixture's coupling to the template hypervisor (the dev-forge-opt-out precedent, and what mkHypervisor needs for platform and hardware), an explicit symlink = true (agenix's default, present on every evaluated secret), and the cost of the string-form sabotage fixtures (same shape as dev-forge-opt-out, forcing only config.age.secrets).

Labels are unavailable to this token; none is warranted.

The hypervisor builder now places two files in the human account's home from its identity: the account's own Forge API token at `~/.config/git/forgejo-token` and the site hosting rclone config at `~/.config/rclone/rclone.conf`, each an agenix-owned symlink, mode 0600, and each only when the identity names an age file for it. Nothing changes for any machine today: the template identity has neither field, the module reads them with `or null`, and the template hypervisor still generates zero age secrets against the current secrets pin. A new check builds the template hypervisor both ways and pins the exact secret set (file, path, owner, group, mode, symlink) plus the generated decrypt activation; it was shown to fail on a wrong mode and on `symlink = false` and pass again when reverted. If this is wrong, a deployment that has set the fields sees the difference only after its own secrets change plus a rebuild; reverting means dropping the one import line in `mkHypervisor`. A deployment that carries its own placement module for these paths deletes it after its pin advance, otherwise agenix declares the same secret twice. Closes allod/archetypes#74. Refs allod/strategy#51, allod/secrets#17. ## What changed - `modules/host-user-credentials.nix`: takes `identity` from `specialArgs`, declares `age.secrets.user-forgejo-token` and `age.secrets.site-hosting-rclone` from `userForgejoTokenFile` and `siteHostingConfigFile`, and refuses a string value with the same path-or-null contract `mkDevVm` enforces (allod/archetypes#70 may relax both together). The header states the agenix facts read from the pinned revision: `installSecret` runs `mkdir -p` on the custom path's parent as root, installs with `ln -sfT` so a hand-written file is replaced in place, and cleanup removes only the previous generation so a dropped declaration leaves a dangling symlink for the operator. - `flake.nix`: `mkHypervisor` imports the module beside `netrc.nix`; a `host-user-credentials` check, modelled on `dev-forge-opt-out`, asserts the null branch (neither secret, no activation text naming either), the fixture branch (literal projection and a grep of the generated agenix install script for both ciphertext basenames, after forcing the toplevel so module assertions run), and the string-form sabotage for each field with a path-form control. - `checks/fixtures/host-user-credentials/*.age`: two placeholder files whose Nix paths the fixture branch uses; the content is a comment, never decrypted. - `README.md`: one sentence under the credentials section. The hypervisor builder composes no `age.identityPaths`; the consumer's own module owns that (nexus `docs/integration.md`, "Consumer-owned configuration"), so the check supplies one through the existing `definition.nixosModules` seam for the fixture only. ## Risk R1. The module is inert until an identity names a file. On a host whose first activation runs before the account exists, agenix creates `/home/<user>` and `~/.config` root-owned ahead of the `users` step, and a root-owned `~/.config` blocks the account from creating anything beneath it until chowned; the dev-machine token module has the same exposure on every fresh dev VM, and a real hypervisor already has the account. The check's fixture branch is the only place a hypervisor with these secrets has been evaluated; no deployment was rebuilt. Not run: `fleet-diff` against a deployment that sets both fields, since none is reachable from this public checkout. ## Validation - `nix build .#checks.x86_64-linux.host-user-credentials` passes on the final commit. - Sabotage runs: changing one secret's mode to `0644` fails the check with `with-access fixture no longer generates today's two secrets...`; setting `symlink = false` on both fails the same way; each reverted and rebuilt green. - `nix build .#checks.x86_64-linux.dev-forge-opt-out` still passes. - `nix eval --json .#nixosConfigurations.nexus.config.age.secrets` is `{}` and the template hypervisor's `system.build.toplevel.drvPath` evaluates against the current secrets pin, which lacks the fields. - Reviewed by codex (gpt-5.6-sol) and pi (gpt-oss-120b) over four rounds. Applied: the projection now pins `symlink`; the header names which tool reads the user token, scopes the activation-order claim to the activation-script path, names `~/.config` itself among the root-owned parents on a first activation, and separates the one-removed and both-removed cases. Rejected against source: the fixture's coupling to the template hypervisor (the `dev-forge-opt-out` precedent, and what `mkHypervisor` needs for platform and hardware), an explicit `symlink = true` (agenix's default, present on every evaluated secret), and the cost of the string-form sabotage fixtures (same shape as `dev-forge-opt-out`, forcing only `config.age.secrets`). Labels are unavailable to this token; none is warranted.
vnprc approved these changes 2026-09-10 16:44:38 +01:00
vnprc merged commit c9578b267c into master 2026-09-10 16:44:43 +01:00
vnprc deleted branch agent/host-user-credentials 2026-09-10 16:44:43 +01:00
Sign in to join this conversation.
No description provided.