bootstrap: create an admin user, and make the root door role-aware #21
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#21
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/bootstrap-admin-user"
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?
Closes #17.
rig hardens the SSH door but never created a human to walk through it: every box was administered as
root, survivable only because of theprohibit-passworddrop-in. This adds an admin user on every role and makes closing root's door role-aware — so a uniform "lock root" can't cut Coolify off from the fleet.Plan
"Lock root" is four different actions, not one
Measured against a live sshd (OpenSSH 10 / Debian 13), not inferred from hardening guides:
passwd -l root(shadow →!*)*PermitRootLogin prohibit-passwordusermod --expiredate 1 root/usr/sbin/nologinchshthen fails too)PermitRootLogin no--lock-rootuses--lock-rootmeans exactlyPermitRootLogin no.usermod --expiredateand the nologin shell don't just break interactive root — they break rig's own convergence, since rig runs as root over SSH and a re-run to pick up a fix would find the door bolted from a direction sshd cannot reopen.PermitRootLogin noleaves the account intact and reopenable by deleting one drop-in.passwd -l rootbuys ~nothing on top ofprohibit-password(already no password surface), so rig skips it and says so.Role-aware policy — why control-plane and workload refuse
--lock-rootcontrol-planehost.docker.internal); non-root localhost unsupported upstream (coollabsio/coolify#4245). A uniform lock-root would cut the control plane off from itself.workloadcoolifyuser withNOPASSWD: ALL(root by another name) rig does not provision. Attribution is cheaper via sshd key-fingerprint logging + auditd. Revisitable.runnerRefusals are hard errors (exit 2), same spirit as
runnerrefusingtag:server— a flag that silently bricks a box's only door is worse than no flag. The workload refusal is a deliberate scope call: non-root Coolify is a big, experimental dependency whoseNOPASSWD: ALLis strictly more privilege than the docker-group membership rig already refuses, so this PR refuses rather than half-provisions it. (devfrom #12 isn't on main yet; it joinsrunneras lockable when it lands.)The admin user (every role)
Non-root account,
sudogroup, neverdocker(root-equivalent socket — same reasonrunner installrefuses it), no other supplementary group. Generic default nameadmin, refusesroot. NOPASSWD sudo — the admin is key-only with no password, so password-required sudo would make it unable to escalate (a non-root user who can't escalate isn't an admin); this is what Debian/Ubuntu cloud images do for their default user, and is right for a human even though it's wrong for Coolify's service user.Seed-from-root = live proof, seeded once
The admin's
authorized_keysis seeded from root's own at creation. The operator is connected as root right now using one of those keys, so the copy is live proof the private key is in their hands — better than any check rig could invent, needs no new argument, and a public key isn't a secret.--admin-keycomposes an explicit key with the seed.Seed-once is a named, honest exception to convergence: re-seeding every run would resurrect a key the operator deliberately removed, so rig seeds only at creation and leaves an existing admin user untouched on re-run. It skips
command=/from=restricted key lines while seeding (they'd silently follow to the admin) and warns; Coolify's own key in root'sauthorized_keysis a documented audit caveat.The lockout checklist — verified before the door closes
Before writing
PermitRootLogin no, rig verifies locally and leaves the door open (exit 1, naming the failed check) if any fails:authorized_keysnon-empty, owned by the admin, not group/world-writable (sshd silently ignores an over-permissive file);sudo -n trueunderrunuser -u <admin>;sshd -T -C user=<admin>resolves to something that permits the login (anAllowUsers/AllowGroups/DenyUsers/Matchblock elsewhere can silently exclude them).What rig cannot verify — that the operator holds the private key — is exactly what seeding from root's keys covers. The drop-in is
00-rig-root.conf, sorting before00-rig.confon purpose: sshd_config is first-wins, soPermitRootLogin noin a10-*file would be read after00-rig.conf'sprohibit-passwordand silently discarded (the same first-wins trap that once cost this repo a month of boxes servingpasswordauthentication=yes). It's installed with the same validate-before-restart +sshd -t+ rollback +sshd -Teffective-assert dance as the base drop-in, and the base assert was widened to acceptnoso a locked box stays convergent.What changed
commands/bootstrap.sh—--admin-user/--admin-key/--lock-rootflags (validated before the root check); role-gated lock-root refusals; admin-user provisioning (useradd + sudo group + NOPASSWD sudoers viavisudo -cf+ seed-from-root); lock-root reachability verification +00-rig-root.confinstall; base hardening assert widened to acceptpermitrootlogin no;sudoadded to base packages.test/cli.sh— arg-surface + refusal tests (below).README.md— admin-user section, the four-techniques table, the role policy table, and the lockout checklist.Testing
bash test/cli.sh→ 75 passed, 0 failed.shellcheck -xoverbin/* **/*.sh→ clean.New unit tests (all reachable without root):
--admin-user rootrefused exit 2;--admin-user/--admin-keyneed a value;--lock-rootrefused exit 2 on control-plane (self-SSH message) and workload (non-root-mode message);--lock-rooton runner parses and falls through to the non-rootmust run as rootrefusal (proving the gate lets runner past); unknown flag still exit 2.Runtime provisioning + lock-root verification are rehearsal-only (the arg surface is fully unit-tested; the rest needs a live box). The Incus container rehearsal can't see this — no cloud-init drop-in, no Coolify. Real rehearsal on a throwaway cloud box:
bootstrap, then from a second terminal confirm inbound SSH as the admin user (the one check rig cannot self-assert), then--lock-root, then re-run rig and confirm it stays convergent through the admin path, asserting effective state (sshd -T -C user=…,sudo -n true) rather than the files rig wrote.Interactions
devrole):box'ssetup-host.shneeds a sudo-capable non-root user to run under; this PR creates it.devwill joinrunneras a lockable role.bootstrap.sh's arg parsing / role handling.Closing as superseded by #30, per its description: main's fleet-users redesign (#27) reshaped #17, and #30 re-delivers the remaining pieces (reachability proofs, @root key seeding, runner-row reconciliation) on the current design. This branch embodies the old single-flag model and is CONFLICTING against main. See the merge-order plan on #29/#30.
Pull request closed