rig users: declarative operators and roles across the fleet
#24
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#24
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
Three operators will share the fleet — multiple boxes each — and today nothing defines who exists on a machine or what they may do. The identity model was reworked in #26 (hybrid access model, decided in its comments): named operator accounts exist on every class, because this tailnet is network-only — no Tailscale SSH — so there is no identity broker at the door, and humans sharing the root login would be unattributable. Humans always enter as themselves and elevate via sudo; root is never a human door. What
classdecides is root SSH's fate afterapply:class=humanmachines close it (rig users close-root→PermitRootLogin no);class=servermachines keep it — root there is the automation identity the control plane (Coolify) SSHes in as, key-only. Rig already creates one user (github-runner, a service account) — this issue makes user structure a first-class, declarative rig concern. Companion traits issue: #26, shipped together as one release.The guiding constraint is rig's charter: public tool, private state. The user file is state — it lives in the private infra repo and is passed in per invocation; rig never persists it and it contains no secrets (SSH public keys only).
Command surface
Conventions as everywhere in rig: run as root (via
sudo rigfrom an operator account, or direct root during bring-up); arg validation before the root check; exit 2 = usage error, 1 = runtime refusal; convergent — second run is a no-op.File format
Line-based and bash-parseable — no YAML, no jq (repo precedent:
json_fieldexists precisely to avoid dependencies).#comments and blank lines allowed.rootmay never appear in the file (refusal).Roles
adminrig-admin%rig-admin ALL=(ALL:ALL) NOPASSWD: ALLrigrig%rig ALL=(root) NOPASSWD: /usr/local/bin/rigboxincusincus-userdaemon confines each member to their own Incus project — no sudo involved at allNotes:
PasswordAuthentication no); users are created with no password and never get one. NOPASSWD sudo is therefore not a weakening — the key at the door is the authentication, same pattern box templates already use (NOPASSWD+lock_passwd: true).incus-adminis deliberately NOT a role. It is host-root-equivalent (full socket: any instance, any config, host mounts) — break-glass tier, granted manually and sparingly. The whole point of theboxrole riding theincusgroup is that the three of us share the dev-server without collisions and without holding the skeleton key./etc/sudoers.d/rig-roles, mode 0440, validated withvisudo -cbefore install (same validate-then-apply shape as bootstrap'ssshd -tgate).class=servermachines the useful role set is typically justadmin(andrig): theboxrole needs Incus, which only hosts have — its absence is already a refusal (see apply, step 3).Behavior of
applysudoif missing and any role requires it (first legitimate appearance of sudo on a rig machine).rig-admin,rigexist; assertincusexists if any user carries theboxrole (the group belongs to the Incus package — if absent, refuse with a pointer to box'ssetup-host, which owns Incus installation; rig does not install Incus).useradd -m, shell/bin/bash, locked password); converge membership in the three rig-managed groups exactly (add and remove); never touch any other group; write~/.ssh/authorized_keys(0700 dir / 0600 file, user-owned) to exactly the file's keys.usermod -L, strip rig-managed groups, keep home) and warn. Deletion is a non-goal — a convergence run must never destroy data.visudo -c; install atomically.Class marker (replaces the old class gate)
The earlier model refused
applyon server-class ("only admins belong there and root already is that"). The hybrid access model inverts that: operators belong everywhere — attribution is the point — soapplyruns on every class. The marker (#26's/etc/rig/role) still matters, at the root-closing step:rig users close-rootrefuses onclass=server— root there is the control plane's automation identity; closing it severs fleet management. No--force.close-rootalso refuses when no marker exists (pre-existing machines): the command that shuts the root door must know which kind of box it is on. Fail closed; re-run bootstrap to write the marker first.applyitself never refuses on class — onclass=serveror an unmarked box it proceeds, noting that root SSH stays until/unless the box is marked human andclose-rootis run.Behavior of
close-rootclass=human(above).rig-adminuser exists with a non-emptyauthorized_keys— never close the only door. The refusal names the repair:rig users applyan admin first.ssh <admin>@<box>works in a separate session before running this./etc/ssh/sshd_config.d/00-rig-users.confcontainingPermitRootLogin no. The name is load-bearing:sshd_configis first-wins and the Include glob expands lexically, and-(0x2D) sorts before.(0x2E), so00-rig-users.confis read before bootstrap's00-rig.confand itsPermitRootLogin nobeatsprohibit-password. Same validate-then-apply shape as bootstrap:sshd -ton the merged config before restarting, roll back on failure, then assertsshd -Tresolvespermitrootlogin no.permitrootlogin (prohibit-password|without-password)) must also acceptno— strictly harder — so re-running bootstrap afterclose-rootstays green and never reopens root. (Small #26-side edit, shipped in the same release.)Root's key hygiene on
class=serveris documentation, not automation: locking root'sauthorized_keysto the control plane (afrom="<control-plane-addr>"clause on Coolify's key line) is the recommended posture and goes in the README — but Coolify owns its key material on the servers it registers, and rig writing root'sauthorized_keyswould be two tools converging one file. Same drift-by-construction argument as rig not touching Incus.Cross-repo dependencies (referenced, tracked in heavy-duty/box)
incus-group restricted tier (incus-userdaemon + per-user projects) is present and functional on the Incus version the M900s run — task 0, before anything else lands.box-netprofile,boxnetnetwork, ACL,user.box=1tags with no owner). Under per-user projects the profile must exist per project andbox listsemantics change. Needs its own box issue.install.shshould grow a root branch mirroring rig's (system tree +/usr/local/bin/boxsymlink). Needs its own box issue.Testing
test/cli.sh: usage paths; fixture files exercising every refusal (unknown role, role mismatch across lines,rootin file, malformed line, missing file) — all non-root, network-free, exit 2 with greppable messages.close-root: usage paths non-root; marker refusals exercised against a fixture marker path; a static lexical assertion that00-rig-users.confsorts before00-rig.conf(the first-wins guarantee the whole command rests on), and an ordering grep thatsshd -tprecedes the restart (repo precedent: the runner repo-guard ordering check).visudo -cgates the sudoers install (same ordering-grep shape).close-rootflipssshd -Ttopermitrootlogin noand a bootstrap re-run does not reopen it.Non-goals
authorized_keys(Coolify owns its key on the servers it manages; thefrom=lock is README guidance, not code).Acceptance
rig users apply --file <fixtures>refusal matrix passes intest/cli.sh;close-rootmarker/ordering assertions pass; shellcheck green via the exact CI invocation.rig users statusreports the truth;close-rooton a human-marked box closes root and survives a bootstrap re-run.box-role user on the dev-server can mint and manage their own boxes and cannot see another user's (pending the box-side dependencies above).Companion traits issue: #26 (supersedes #25) — one release together.
Follow-up decided in #26: on
class=humanmachines, closing root SSH entirely (PermitRootLogin no) belongs here, not in bootstrap — bring-up happens over root SSH before any user exists, so it only becomes safe afterrig users applyhas converged an admin user with a working key. Not part of this issue's scope; noting it so the follow-up has a home.