Derive VM target and host-key pin from the deploy flake in rebuild-vm-from-host and provision-vm-from-host #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rework
scripts/lib/rotation-common.sh(and its callersscripts/rebuild-vm-from-hostandscripts/provision-vm-from-host) so a single deploy-flake reference is the source of truth for the target IP, the username, and the host-key pin — invokingrebuild-vm-from-host <vm>orprovision-vm-from-host <vm>should need no manualINVENTORY=,MACHINE_PROFILES=, orIDENTITY_CONFIG=.Context
Both scripts currently source what they need from three independently-maintained checkouts selected by environment variables.
MACHINE_PROFILESis the flake that gets built (--flake "${MACHINE_PROFILES}#<vm>") and also the source of thesecretsinput revision read from itsflake.lock.IDENTITY_CONFIGis the secrets checkout, used both for the username (nix eval path:${IDENTITY_CONFIG}#lib.vmUsernames.<vm>) and for the host-key material (git show <secrets-rev>:machine-host-keys.json).INVENTORYis the inventory checkout, used for the target IP (${INVENTORY}/scripts/vm-specs.json).Under the private-first template model an operator builds a deploy flake that already pins the correct
inventoryandsecretsrevisions and composes them with the public profiles/nexus/vm framework. The provisioning scripts do not read from that flake; they read the three side checkouts, which the operator must point and keep current by hand. In practice these drift and surface as failures that look unrelated to the real cause: a build flake whose default (public template) secrets pin does not match the operator's real secrets checkout producespinned secrets@<rev> is unavailable locally; anINVENTORYpointed at the public template inventory yields a placeholder target IP andssh-keyscan returned no presented SSH host key; a staleINVENTORYcheckout missing a newly added machine yieldsunknown VM '<vm>'. Each is a mispointed or stale checkout rather than a build problem — the deploy flake itself builds every machine correctly.Scope
The first change is in
scripts/lib/rotation-common.sh: replace the "resolve the secrets pin fromMACHINE_PROFILES/flake.lockand read files from separateIDENTITY_CONFIGandINVENTORYcheckouts" logic with derivation from one deploy-flake reference. The two callersscripts/rebuild-vm-from-hostandscripts/provision-vm-from-hostthen pass that single reference instead of three checkout paths. The deploy-flake location must stay configurable with the same env-override shape the current variables use, since this is public tooling and each operator points at their own deploy flake.Default approach: resolve the deploy flake's
flake.lockto its pinnedinventoryandsecretsrevisions, then readvm-specs.jsonandmachine-host-keys.jsonat exactly those revisions (same repos, but always the pinned commits instead of whatever a side checkout happens to be on). This keeps local checkouts but removes the drift, and is the smaller change.Follow-up alternative, only if the default proves unworkable: expose the target IP and the pinned host keys as flake outputs so the scripts use
nix eval <deploy>#...with no side checkouts at all. This eliminates checkout state entirely but requires adding read-only outputs to the profiles, inventory, and secrets flakes and is a larger, cross-repo change; leave it as a follow-up.Validation
rebuild-vm-from-host <vm>andprovision-vm-from-host <vm>complete with noINVENTORY/MACHINE_PROFILES/IDENTITY_CONFIGset, against a configured deploy flake, with target IP, username, and host-key pin all derived from that flake. Updatetests/rebuild-vm-from-host.sh,tests/provision-vm-from-host.sh,tests/rotation-common.sh, and the sharedtests/provisioning-contract.shto cover the new derivation; the token-rotation flow that also sourcesrotation-common.shmust still passtests/rotate-token.sh. The actual VM rebuild/provision against a live host is a manual host-side gate for the human and is not part of automated validation.Boundaries
Preserve the host-key pinning preflight (
assert_any_vm_host_key_material_pinnedandassert_vm_host_key_material_pinned) at equal strength: it is a security control that stopsnixos-rebuild switchfrom landing on a host presenting an unexpected or absent SSH host key, so re-sourcing its inputs must not weaken it, and a regression test that the preflight fails closed on an unpinned key is required. Out of scope: the deploy-flake composition itself, the profilesprofileDefinitionsmodel, and the inventory and secrets data schemas (except adding read-only outputs if the follow-up alternative is chosen). Host-side only: no live rebuild or provision from a dev VM. Keep fixtures on the public template inventory/secrets; no host-specific addressing or secrets in code or tests.Design status
The default-vs-follow-up choice is a genuine design fork with a security-sensitive component, so treat this as design-then-implement: the dev plan should pick the approach, enumerate the exact touched functions in
rotation-common.shand their new inputs, and define the test matrix before any code lands.Resolved by nexus commit
cb0758a— "Derive provisioning source from deploy flake". The code diff is at that link.Process note — the PR was accidentally skipped: this landed directly on
masterinstead of going through a pull request. Branch protection has since been enabled onnexusmaster, so future changes require a PR. Because there was no pre-merge PR to review, an implementation-review pass was run after the fact against the converged dev plan (nexus-deploy-flake-provisioning-sourceinallod/strategy, converged through three rotated plan-review passes: claude-fable-5 → claude-opus-4-8 → gpt-5.5).Post-hoc implementation review (claude-opus-4-8) — clean, no changes warranted:
nix flake checkis green, and a forced fresh rebuild ranshellcheck -x+bash -n+ all ten test suites in-sandbox, all passing.nixos-rebuild/new-vm/nixos-anywhereon any absent, mismatched, or entry-without-material key, and the suites assert that refusal together with an explicit "no build ran" check. No fail-open path exists.forge_keyis resolved up front at the inventory pin so the late bootstrap gate can't die on an unbound variable, with an anti-drift assertion (a pinned non-null value beats a working-tree null).DEPLOY_FLAKE/INVENTORY_CHECKOUT/SECRETS_CHECKOUTwithINVENTORY/MACHINE_PROFILES/IDENTITY_CONFIGremoved from both scripts; the changedrotation-common.shhelpers have no callers outside the two in-scope scripts, sorotate-tokenand the out-of-scope scripts are unaffected; and the zero-env goal is proven viaenv -uplus fixtures planted under$HOME.One immaterial note (not a defect, no code change): with an explicit positional IP override, an unknown VM now fails one step earlier — at the username eval ("cannot resolve username for …") rather than the host-key assert the plan prose predicted — still loud and fail-closed before any build.
Closing: implemented and independently reviewed clean.