bootstrap: infer the tailnet tag from the pre-auth key — verify the tag granted, not the one requested #20
No reviewers
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:bootstrap
scope:coolify
scope:db
scope:docs
scope:drill
scope:installer
scope:labels
scope:platform
scope:runner
scope:users
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/rig#20
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/bootstrap-infer-tailnet-tag"
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?
Plan
rig bootstraptook--ts-tagand passed it totailscale up --advertise-tags— but a pre-auth key already carries the tags it was minted with. The tag was stated twice, and rig had no idea whether the two agreed. This is the sshd first-wins bug wearing a different hat: rig asserted the tag it requested, never the tag control granted. The fleet already has the scar — both M900s carriedtag:serverand had to be retagged by hand; nothing in rig noticed because nothing ever looked.Collapse the two sources of truth onto one — the key — and verify the granted tag, mirroring the
sshd -T"assert the EFFECTIVE state, not the file we wrote" fix:tailscale updrops--advertise-tagsentirely. The key's own tags apply (documented default for a tagged key).tailscale status --jsonfor.Self.Tags— the netmap's ground truth, nottailscale debug prefs, which printsAdvertiseTags(what was requested, the very thing we're deleting). Tags arrive with the netmap, not synchronously fromup, so a single read can legitimately be empty; poll until.Self.Tagsis non-empty or.BackendState == Running, then decide.--advertise-tagsalso drops the accidental net that used to tag an untagged key's node anyway. An untagged node joins owned by the key creator's user identity (inherits that human's ACL grants, dies with the key, vanishes if the account is deleted) — a fleet-shaped mistake, not a warning. rig runstailscale logoutto back the half-joined node out, then dies naming the fix (mint a tagged key). A wrong tag can't converge in place (tailscale sethas no tag flag; re-tagging needsup --force-reauthwith a fresh key), so rig detects and refuses, never claiming a convergence it can't perform.tag:server" but "the key you actually used must not granttag:serverto repo-controlled code." Verified on the already-joined path too, so a box bootstrapped before this change — or retagged behind rig's back — is caught on the next ordinary re-run.--ts-tagis REMOVED, not demoted. It dies exit 2 with a message pointing at the key (consuming its value), rather than an "unknown flag" that leaves an operator guessing where the tag went. (An--expect-tagassertion was considered and rejected — it would reintroduce the second source of truth this change exists to delete.)What changed
commands/lib/runner-config.sh— newjson_string_array <file> <key>, the array-aware sibling ofjson_field(jq-free grep/sed, one element per line, empty when absent, never fails underset -e)..Self.Tagsis an array;json_fieldonly reads strings.commands/bootstrap.sh— sources the lib (addsHERE+ source line); removes--ts-tagas a real option and itsTS_TAGdefaults;--ts-tagnow dies exit 2 with the "tag comes from the key" message;tailscale updrops--advertise-tags; addsverify_effective_tag(bounded poll of.Self.Tags, untagged→logout+die, runner+tag:server→die, logs the verified tag) called on both the fresh-join and already-joined branches;usage()rewritten.test/cli.sh— replaces the old runner-refuses-tag:server-through---ts-tagtest with the--ts-tag-is-removed exit-2 assertions (value present and absent); adds unit tests forjson_string_array(fixture netmap with a Tags array + a peer to prove first-wins reads Self's, and a no-Tags fixture → empty).README.md— rewrites the--ts-tagbullet and the runner-tag paragraph to describe the new model (key is the source of truth; verify granted, not requested; untagged = hard refusal).Testing
bash test/cli.sh→ 71 passed, 0 failedshellcheck -xoverbin/* **/*.sh→ cleanThe array reader is unit-tested directly (the bootstrap path that calls it needs a real tailnet the harness can't fake). The effective-tag role policy is rehearsal-only for the same reason — it belongs to the 3-key rehearsal:
verify_effective_taglogs it, exit 0.tag:server→ hard die naming the CI-tag repair.tailscale logout, hard die naming "mint a tagged key".Interactions with other PRs
Conflicts textually with the
devrole PR (#12) and the admin-user PR (#17), since all three editbootstrap.sh. This PR changes the tag model, so ordering matters: if #12 (dev role,tag:local) merges after this, dev's tag policy must move onto the same effective-tag check (verify_effective_tag), not a request-time--ts-tag/--advertise-tagspath — which no longer exists.Closes #16.