Retire rotate-token; refresh-local-auth becomes its own host script that accepts both registry shapes #55
No reviewers
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/nexus!55
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/retire-rotate-token"
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?
On a host that has picked up this nexus pin,
rotate-tokenis gone and value rotation isallod secret rotate <name>alone. Refreshing the host's netrc bundle after such a rotation becomes a script of its own name,refresh-local-auth --group <alias>, which does exactly whatrotate-token refresh-local-authdid (root and user netrc files installed under sudo, backed up and rolled back as a set) and now also accepts a source credential already in the registry's new shape, which the old validator refused. Nothing else on the host changes: the other provisioning scripts, the secrets, and the deployed files are untouched until an operator runs the new script. Proof is the script's own test suite (18 tests, including a new-shape source installing byte-identical to a legacy one and every refusal), shellcheck, the flake's scripts check, and two cold reviews by models that did not write it. Do not merge before the replacement is proven live: runallod secret rotate --dry-runand then a real rotation on a group withoutlocal_auth_refreshentries, with the printed verification, first. If this is wrong after merging, revert the commit and move the nexus pin back; nothing this PR ships writes anything until invoked.Decision: where refresh-local-auth lives
Its own host script in
scripts/refresh-local-auth, not a subcommand of arotate-tokenthat no longer rotates. A script named for what it does is what a reader ofallod secret rotate's printed steps expects to find, and the cost is one string on the tools side, changed in the companion PR (allod/tools PR 210). No compatibility shim is kept: the only window in which the old spelling could be printed for a host that lacks it is the one the pin order below avoids, and the failure there is a loud "command not found", not a silent wrong install.What changes
scripts/refresh-local-auth(new):--group <registry-alias>and--help. Registry read, unknown-group refusal, wrong-host refusal, sudo preflight before any decrypt, age decrypt into a variable, the one-linehttps://<user>:<token>@<host>parse, the four-file bundle (/root/.git-credentials0600,/etc/nix/netrc0640 root:users,/root/.netrc0600,/home/<user>/.netrc0600) with backup and full rollback, and the idempotent re-run are carried over unchanged. Two things are tightened after review: the plaintext workdir is the lib'smake_tmpfs_workdirunder/dev/shm(overridable withREFRESH_LOCAL_AUTH_TMPDIR, refusing a non-tmpfs parent unlessREFRESH_LOCAL_AUTH_ALLOW_NON_TMPFS=true), as every other rotation script here does; and every refusal that needs no plaintext (wrong host, privilege, each destination's parent directory, the local user's existence) is settled before age runs. A symlink at any destination, dangling or not, is refused before anything is decrypted or backed up: the archetypes netrc activation script and agenix both install these files as regular files, and following a link would have restored the old credential as a regular file at the link's own mode on rollback. Test hooks are renamed fromROTATE_TOKEN_*toREFRESH_LOCAL_AUTH_*.local_auth_refreshentry's contract, system, user name, and source credential, and that the source is a credential-store URL source with one/root/.git-credentialstarget on that system. A source qualifies in either shape: legacyformat: credential-store-urlwith novalue, or new-shapevalue.templatewith noencode, exactly one{secret}, and exactly one non-blank line (blank meaning spaces and tabs only) matching the same URL grammar. The predicate mirrorsisCredentialStoreUrlSourcein the archetypes flake andisCredentialStoreURLSourceinallod secretcharacter for character. The old validator also required legacyformatandverify.typeenums on every credential, which is why it refused the current public registry.scripts/rotate-tokenandtests/rotate-token.share deleted.tests/refresh-local-auth.shreplaces the latter with the refresh-relevant tests plus the new-shape cases.flake.nixnames the new script and test in the scripts check;nix/provisioning-scripts.nixdrops the rclone wrapper that existed only for stanza rotation, and with itmakeWrapper.docs/credentials.mddescribes the current registry shape (value.template, optionalvalue.encode, oneverifycommand per target) and points rotation atallod secret rotate;docs/integration.mdsays the rclone password rotates throughallod secret rotatewithvalue.encoderclone-obscure.Ordering and agent gates
The
allod-toolspin lives in the deployment's flake, not here. Three human steps, in order:allod secret rotate(allod/tools master at or after allod/tools PR 208) and prove it live on a group withoutlocal_auth_refreshentries:allod secret rotate --dry-run <name>, the real run, the printed verification. Only then merge this PR.refresh-local-auth --group <alias>and letsallod secret migratehandle alocal_auth_refreshsource only whenrefresh-local-authis on PATH; a host with the new tools and the old nexus would print a step that does not exist, and a host with the old tools and the new nexus can still rotate every group but cannot migrate the local-auth one yet.allod secret migrate <name>, rotate it withallod secret rotate, and run the printedrefresh-local-auth --group <alias>.Risk
R3: a host script that writes root-owned authentication files under sudo, plus cross-repo sequencing with the tools change. Residual after the tests: the predicate and the archetypes check could drift apart in a future edit (each is commented with the other's name, and the archetypes check runs on every secrets change); and a plaintext that parses but carries a trailing newline is installed as-is, exactly as before. Rollback is a revert plus moving the nexus pin back; the script writes nothing until run.
Validation
Run by the overseer in the worktree on the committed tree (
be918d4, three commits):bash -n scripts/refresh-local-auth tests/refresh-local-auth.shbash tests/refresh-local-auth.sh18 refresh-local-auth tests passed.bash tests/rotation-common.sh32 rotation-common tests passed.nix build --no-link -L .#checks.x86_64-linux.provisioning-contract(runs shellcheck and every test)nix build .#packages.x86_64-linux.provisioning-scriptsthenls binrefresh-local-authpresent,rotate-tokenabsentgit diff --checkgrep -rn 'rotate-token|ROTATE_TOKEN' --exclude-dir=.git .The worker additionally sabotaged the script eleven ways (predicate accepting everything or
encode: false, preflight skipped, parent-directory check removed, rollback removed, netrc mode widened, empty-list check removed, tmpfs workdir replaced by baremktemp, symlink refusal removed, local-user check moved after the decrypt) and reported a different test failing each time, with the script restored byte-identical afterwards.Cold reviews, each reading a
.git-free snapshot and the diff against master, both from the GPT family because the change was authored by a Claude model. First pass on the first commit:gpt-5.6-terraat thinking high through pi andgpt-5.5at effort high through codex. Between them they found, all verified against the source and fixed in the second commit with a test each:value.encode: falseaccepted by jq's//while the archetypes predicate rejects it; the plaintext workdir was a baremktemp -dinstead of the lib's tmpfs helper every other rotation script uses; age ran before the destination directories and the local user were checked; and a dangling symlink destination was recorded absent and unlinked on rollback. One terra finding was discarded: jq and Nix differ on Unicode whitespace inside the URL grammar, which is a fail-closed refusal on an input nobody writes. Second pass on the fixed tree bygpt-5.5through codex found one high (a non-dangling symlink destination would be restored as a regular file at the link's 777 mode on rollback) and one low (no test proved the user check ran before decrypt); both fixed in the third commit, the first by refusing any symlink destination outright since the framework installs these files as regular files. The third commit is a small fix I verified by reading the diff and running the suite and its sabotage lines; no third cold pass, per the review budget for this risk level.Not run: a live refresh on a real host, which needs the age identity and sudo. The first live run should be on the host after step 1 above, with the four destination files compared before and after.
Closes allod/nexus#52
Refs allod/tools#197