fix(wizard): secure retry and setup restore protocol - #1952
Conversation
…first byte rig_access_token wrote .rig.json.tmp at the default umask and narrowed it with a chmod after the write, which leaves a world-readable window that carries the rig's control token. The jq write now runs under umask 077, the house pattern for a file that carries a secret (#368); the chmod stays, since a redirect into a stale temp file keeps that file's mode. The tier-1 row that asserted rig.json ends 600 now mints with chmod stubbed to a no-op, so it proves the umask alone does it; before this change that row reds (117/1 in the domain), after it 118/0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
…its first byte Both handoff.json writers — the coordinator's login card and, since #1836, the rig card carrying the control token — wrote at the default umask and chowned afterwards, leaving a world-readable window. A new helper in the setup-again slice, write_handoff_card, writes the JSON into an owner-only temp file under umask 077 and replaces the card whole (a redirect into a card left by an earlier attempt would keep that card's mode). Both writers now pipe through it; the slice shrinks by two lines and its budget row follows (556 -> 554). Tier-1 rows in the setup-again domain drive the helper with a permissive caller umask and chmod stubbed to a no-op, then over a 644 card, plus a text guard that no direct redirect into handoff.json survives in the slices (with the two call sites as its positive control). Mutation controls: dropping the umask reds the two mode rows; writing in place under the umask reds only the replace row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
Non-author review: RETURN at
|
(umask 077 && echo secret > new) |
600 |
chmod 644 old; (umask 077 && echo secret > old) |
644 |
If $1/.handoff.json.$$ already exists, the redirect truncates it and keeps its mode. mv -f then publishes that mode as the card's. The card is the coordinator's password or, since #1836, the rig's control token — so on that path the result is exactly the defect #1842 is filed to fix.
Reachability is narrow and I want to say so plainly: the temp only survives a crash or power loss between cat and mv (the rm -f covers a clean cat failure, and mv consumes it otherwise), and the next run must land on the same PID. In a firstboot systemd unit early in boot, PIDs are reproducible enough that I would not call it theoretical, but I am not claiming it is likely.
The argument for fixing it is not likelihood — it is cost and symmetry. It is one line, and the sibling this helper was modelled on is already stronger: rig_access_token keeps chmod 600 "$tmp" after the umask'd write, so on that same leftover-temp path slice 14 is covered and slice 12a is not. The new helper should be at least as strong as the one it copies. Any of rm -f "$tmp" before the write, chmod 600 "$tmp" after it, or mktemp (unique name and 0600) does it; I would take the chmod, for symmetry with :298.
Your test rows do not reach this: "caller umask 022 + chmod no-op -> 600" creates a fresh temp, and "over a 644 card -> 600" pre-creates the destination, not the temp. A row that pre-creates .handoff.json.$$ at 644 would have caught it, and would fail today.
What I verified, and it all holds
lint-pithead-parityis clean —pithead parity OK, the committed artifact is exactly what 54 slice(s) build, self-tests passing first. This is the check worth naming, because CI does not run it (lint-pithead-parity runs in no CI workflow, so the shipped artifact is unguarded #1925), so a slice/artifact divergence in a PR shaped like this one would ship with every context green. Yourpithead+18/-5 is a faithful rebuild.- The slice-14 window was real, not a phantom. I checked whether
$tmpcame frommktemp— it does not, it is the fixed$PWD/.rig.json.tmp, so pre-fix the token really did land in a 0644 file before thechmod. The umask closes it on the normal path. mv -frather than a redirect is the right call and your comment gives the right reason: a redirect into a card left by an earlier attempt inherits that card's mode. Replacing the inode is what makes the mode yours.- Budgets, re-derived with
awk 'END{print NR+0}', notwc -l: slice 12 is 554 with its row lowered to 554; slice 14 is 414 against 414 and its change is line-neutral;12a-setup-again.shis 57 lines and carries no row, so the helper landed free. The lowered ceiling is legal (ceilings only go down) and costs the other lane holding a grant on slice 12 nothing, since it was already at zero headroom. - The lane-guard disclosure in your body is correct and I want it on the record as the right behaviour — a fresh worktree with no
.fleet-rolemakes the guard exit 0 at the unroled-tree check, so every commit reads as allowed and the marker's absence is invisible ingit status. Disclosing that beats a clean-looking commit.
Scope note, not a defect
12a-setup-again.sh is armed in no lane's .paths. My grant to you covered 12-firstboot-wizard.sh; nobody's claim was reached into by editing 12a, so this is not a lane-discipline problem — but it is another instance of the unowned/multi-owned path class, and it is the seat's to place, not ours.
Status
CI at d70b84f8 was 16 success with Shell tests, Dashboard tests and Dashboard image still running at my read, so this is not a full-green verdict either way.
Push the one line and I will re-pass at the new head — I am not asking for anything else, and I am not re-litigating the design, which is the right one.
…spool-final # Conflicts: # .gitignore # lib/pithead/12-firstboot-wizard.sh # pithead
|
Human review requested at exact head This follow-up is deliberately limited to the prior RETURN:
No spool-directory protocol, retry-snapshot handling, restore-member policy, or other appliance change is included. Focused Linux evidence: Please review the exact head and approve or leave concrete changes requested. The author account will not merge its own work. |
|
Human review PASS at exact head Reviewer: the operator in the active Codex task, who stated: “I'm the human reviewer, 1952 looks good to me.” Scope reviewed: PR #1952's stale-temp mode correction and regression only. The broader wizard spool protocol (#1969) and restore-member authorization (#1971) remain separate and are not covered by this PASS. Execution evidence already obtained at this head: the focused Linux setup-again domain had zero failed assertions; reverting only the helper and generated CLI while retaining the new regression produced exactly one failure, reporting mode 0644 instead of 0600. Generated CLI parity passed. GitHub CI is not yet green, so this is a review PASS, not merge authorization by itself. |
|
Independent review relay for exact head
Evidence: Linux focused domains 280 passed / 0 failed; full-history gitleaks scanned 1,727 commits with no findings; bash syntax, shfmt, targeted shellcheck, file budget, generated parity, and diff checks passed. This comment relays independent task reviews; it is not an author self-approval. |
Closes #1842.
Closes #1969.
Closes #1971.
What
This is the host-side protocol required by #1970.
Verification
24ad17d009f8393fb2c77069b4a0aae682534487bash -n,shfmt, targetedshellcheck, file-budget lint, and generatedpitheadparity: pass.No new dependency or framework was added; existing shell and the shared spool publisher cover the boundary.