Machine traits: roles as presets over class/host/join (supersedes #25) #26
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#26
Loading…
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?
Context
#25 made machine class bootstrap's organizing axis: server-class vs host-class. Working staging and workstations through that model showed the binary was bundling three independent questions that merely correlated across the first four roles:
rig usersby virtue of being host-class, which nobody needs there. Its guests are already root-only (heavy-duty/box#69); the corrected model makes host and guests both unattended, so the recursion gets cleaner.This issue supersedes #25: the class binary becomes a small set of orthogonal traits. Roles stay — they are the presets that make the usual shapes one command — but every trait is individually settable, so rig can also bootstrap a custom shape. A role is a template, not a straitjacket.
The traits
classhuman/serverrig users(#24) — humans always enter as themselves; root is never a human door (see the hybrid access model under Decided). Whatclassdecides is what happens to root SSH afterrig users apply:human— closed entirely (rig users close-root→PermitRootLogin no);server— stays open as the automation identity the control plane SSHes in as, andclose-rootrefuses.hostyes/no/dev/kvmadvisory and the closing pointer at boxsetup-host. A host's traits say nothing about its guests' — staging guests are full machines converged asworkload.joinauthkey/loginauthkey: tagged pre-auth key, node is tag-owned; untagged is refused and backed out (today's behavior).login: interactivetailscale up, node is user-owned; here the assertion inverts — an effective tag is the refusal. Same mechanism either way: verify what control granted, never what was requested.tag:server policy stays derived, not a trait: allowed only on the machines the control plane manages (control-plane, workload); every other shape refuses it on the effective tags. This generalizes today's runner and staging checks into one rule.
Roles as presets
control-planeworkloadrunnerstagingdevworkstationThe correction vs #25: staging is class=server. It hosts VMs but is unattended — a VM appliance. "Hosts VMs" was never the load-bearing property for identity; it just correlated.
Any trait is overridable on any role —
rig bootstrap workstation --host nofor a machine that will never mint boxes — andrig bootstrap custom --class <c> --host <h> --join <j> --hostname <name>builds a shape with no preset at all (custom has no default hostname;--hostnameis required). The marker (below) records effective traits, so an overridden role never lies about what the box is.What changes
commands/bootstrap.sh— the single place a role's shape is declared. All behavior that #25 keyed per-class keys off traits: kvm advisory (host=yes), tag:server refusal (derived policy), next-steps log (host=yes→ box setup-host pointer;rig userspointer per the identity model).devandworkstation. Thejoin=loginpath: no pre-auth key demanded (a setTS_AUTHKEYis a loud refusal telling you to unset it or pass--join authkey), interactivetailscale up, then assert the node is user-owned and untagged./etc/rig/role, e.g.role=staging class=server host=yes join=authkey— convergent like the hostname. First consumer: #24 —rig users close-rootrefuses onclass=server(root there is the control plane's automation identity, not a human door), whileapplyruns on every class.Non-goals
rig usersimplementation (#24; the marker is its input — see the hybrid access model under Decided for what each class means there).Decided
rig usersrefuses"; worked out in the comments below). Named operator accounts exist on every class: this tailnet is network-only — no Tailscale SSH — so there is no identity broker at the door, and multiple humans entering as root would be unattributable. Humans therefore always enter as themselves and elevate via sudo, on servers too. Root SSH is whatclassdecides afterrig users apply:human→ closed entirely;server→ stays, as the automation identity the control plane (Coolify) SSHes in as — key-only, and never a human door. Side benefit: once humans never use root, any root login that isn't the control plane is anomalous by definition — a cheap, high-signal alert that a shared root identity makes impossible. Caveat kept honest: on Docker-running boxes this buys attribution, not privilege reduction — an operator with sudo is root-equivalent; attribution is the goal.PermitRootLogin noonclass=humanis arig usersfollow-up, not a bootstrap concern. It cannot happen at bootstrap — dev bring-up is over root SSH before any user exists. Bootstrap keeps the fleet-wideprohibit-password; closing root SSH becomes possible only afterrig users applyhas put an admin user with a working key on the box (rig users close-root, specified in #24).Open questions
hostreclaims the word the fleet already uses ("the box that hosts staging boxes",box setup-host); alternativevm-hostif the bare word reads too generic.Acceptance
rig bootstrap devandrig bootstrap workstationexist and behave per the preset table;test/cli.shcovers both asstagingis covered, plus: a trait override changes the derived behavior,customwithout--hostnameexits 2,join=loginwithTS_AUTHKEYset exits 2.bash test/cli.shgreen.Supersedes #25 (class-based model). Companion users issue: #24 — ships with this one as a single release. Prior related: #22 (staging role, PR #23), heavy-duty/box#68 (staging template, PR #69).
Revisiting one decision in this design after a closer look at the access model: operator (human) entry to server-class machines.
The current text says:
and #25 adds "operators enter as root." That works for the automation half (the control plane SSHing as root with its own key is machine-identity access — attributable to the software, logged by it). But the human half inherits the classic shared-root problem: multiple operators entering as root means logins that can't be tied to a person. Tailscale here provides the private network only — not Tailscale SSH — so there's no identity broker to recover attribution at the door.
Proposed change: hybrid access model on server-class
Keep root as the automation identity; add named operator accounts for humans.
PermitRootLogin prohibit-password, and root'sauthorized_keyscontains only the control-plane key — with afrom="<control-plane-addr>"restriction on the key line (or an equivalentMatch Addressblock) so it's unusable from anywhere else.What this changes in the trait model
rig usersruns on server-class instead of refusing — presumably a minimal operator set with prod-appropriate sudo scope, vs. the fuller dev/workstation setup.classno longer decides whether users exist; it decides what happens to root SSH afterrig users apply:human→PermitRootLogin no(unchanged from current plan)server→ root stays, key-locked to the control planerig usersfollow-up (#24).Side benefit
Once humans never use the root identity, any root login not originating from the control plane is anomalous by definition — a cheap, high-signal alert that's impossible when humans and automation share root.
Caveat for honesty: on Docker-running boxes this buys attribution, not privilege reduction (an operator with sudo is root-equivalent anyway). Attribution is the goal here.
🤖 Generated with Claude Code