bootstrap: create an admin user, and make the root door role-aware — a uniform 'lock root' would cut Coolify off from the fleet #17
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#17
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?
rig hardens the SSH door but never creates a human to walk through it. Every box rig builds is administered as
root, and the only reason that is survivable is theprohibit-passworddrop-in. "Create an admin user and lock root" is the obvious next step — and half of it is a trap that would cut the control plane off from the fleet.The trap, measured rather than assumed
"Lock root" is not one action. It is four, and they do not behave alike. Verified empirically against a live sshd (OpenSSH 10.0p2 / Debian 13), not inferred from hardening guides:
passwd -l root(shadow →!*)PermitRootLogin prohibit-passwordusermod --expiredate 1 rootAccess denied ... by PAM account configuration/usr/sbin/nologinThis account is currently not available.PermitRootLogin noPermission denied (publickey)passwd(1)is explicit that locking a password is not disabling an account: "this does not disable the account. The user may still be able to login using another authentication token (e.g. an SSH key). To disable the account, administrators should useusermod --expiredate 1."shadow(5)agrees: a!or*password field means "the user will not be able to use a unix password to log in (but the user may log in the system by other means)."So the danger is precisely the guides' advice. Every hardening checklist pairs
passwd -l rootwithusermod --expiredate 1or a nologin shell. The first half is a no-op on a box whose root has no password anyway (Debian cloud images ship*); the second half is what takes the fleet down. If rig grows a--lock-root, it must mean exactly one of these, and say which.Why
PermitRootLogin nois not available on every roleCoolify needs root SSH, and more specifically than the README says:
host.docker.internal— server 0 is not special-cased in the auth path — andscripts/install.shhard-fails off root (if [ $EUID != 0 ]; then ... exit; fi, carrying a literal# TODO: Ask for a user). Non-root localhost is unsupported (#4245, open).v4.0.0-beta.259, 2024-04) but is still labelled experimental inv4.1.2, and requiresyour-user ALL=(ALL) NOPASSWD: ALL— structurally, because Coolify SSHes with-o RequestTTY=noand never pipes a sudo password, so a prompt could not be answered. Docs; installation docs still say "Log in as the root user (non-root users are not fully supported yet)."workloadCAN drop root — via Coolify's non-root mode — but it is not a privilege reduction, and probably not worth it yet. This corrects an earlier claim in this issue that workload's root door "must stay". It doesn't have to; the question is whether closing it buys anything.Verified in Coolify source (
v4.1.2), not inferred:remote_process()— the central command executor — appliesparseCommandsByLineForSudo()wheneverServer::isNonRoot()(a plainuser !== 'root'string compare,app/Models/Server.php:1416).InstallDockerruns through it, so Coolify installs Docker itself on a non-root box viasudo curl … | sh. The box does not need Docker pre-installed.your-user ALL=(ALL) NOPASSWD: ALL— required structurally, since Coolify SSHes-o RequestTTY=noand never pipes a sudo password. The docs call this "not the most secure way… we will improve this in the future, by adding more granular permissions on binaries."So it works. But three things make it a poor trade today:
NOPASSWD: ALLis root-equivalent. Thecoolifyuser cansudo -iat will — strictly more privilege than the docker-group membership rig already refuses inrunner installas "a gratuitous path to root". Attribution becomes cooperative, not enforced: it records what Coolify did in normal operation, not what an attacker who took the account did.docker exec→sudo docker exec. Experimental, and labelled so in the UI two years after it landed (v4.0.0-beta.259, 2024-04).What closing root actually buys — and a cheaper way to get it. The goal is attribution: "was that Coolify or a person?" That is answerable today, for free — sshd logs the key fingerprint on every login (
Accepted publickey for root … SHA256:<fp>), and Coolify's key is distinct from the operator's.auditdadds per-command attribution without touching Coolify's code path. The only thing non-root uniquely unlocks over that isPermitRootLogin no— butprohibit-passwordalready means key-only root with no password surface, so the marginal gain is small against an experimental dependency and aNOPASSWD: ALLthat is root by another name.Recommendation: leave
workloadonprohibit-password; get attribution from sshd fingerprints +auditd. Revisit if Coolify ships the promised granular-sudo permissions — that is when non-root stops being root-with-extra-steps. The row below reflects this as conditional, not forbidden, so the decision is the operator's and stays revisitable.So the root door closes per role, and the constraint is real only where something depends on it:
control-planeprohibit-password)workloadNOPASSWD: ALL; attribution is cheaper via loggingrunnerdev(#12)A uniform
--lock-rootwould still be a fleet outage — it would take outcontrol-plane. A role-aware one is safe, and onworkloadit should be an opt-in the operator reaches for deliberately, not a default rig applies.Proposed
1. An admin user, on every role. A non-root account with
sudo, an SSH key, and no supplementary groups beyondsudo— neverdocker, same reasoning asrunner install.runner installalready sets the precedent for user creation on a rig box (useradd --create-home,runuser, refusesroot), so this is a known shape, not a new one.2. Role-aware root policy.
runneranddevmay close root's SSH door freely.control-planemust not, and rig should refuse rather than warn if asked — the same wayrunnerrefusestag:server; a flag that silently bricks the control plane's access to itself is worse than no flag.workloadis the middle case: closing root there is possible (Coolify non-root mode, see above) but not the default and not recommended yet — if it's ever offered, it must be an explicit opt-in that also provisions thecoolifyuser +NOPASSWDsudoers, never something--lock-rootdoes implicitly.3. If "lock root" ships at all, it means
PermitRootLogin noon the permitted roles — notusermod --expiredate, not a nologin shell, both of which break rig's ownbootstrapre-run path too (rig is run as root over SSH; convergence dies with the door).passwd -l rootcan ship everywhere as a convergent no-op-in-practice, but be honest in the README that it buys approximately nothing on a box whose root has no password to begin with.The lockout problem — and a way out
Closing root's door on a box whose admin key does not work means rescue mode. rig can verify a lot locally: the account exists and is unlocked,
authorized_keysis non-empty with sane ownership/perms,sudo -n truesucceeds underrunuser, the shell is valid, andsshd -T -C user=<admin>resolves to something that actually permits the login (anAllowUsers/Matchblock elsewhere can quietly exclude them). What rig cannot verify is that the operator holds the private key. Only an inbound connection proves that.Seeding the admin's
authorized_keysfrom root's own turns that unprovable claim into a proven one. The operator is right now connected as root using one of those keys — that is live evidence the private key exists and is in their hands, which is strictly better than any check rig could invent. It also needs no new argument, and a public key is not a secret, so nothing about "no credential, ever" bends.Two wrinkles that need a decision, not a default:
authorized_keyswhen it registers a server. Blind-copying hands the admin account to Coolify's key. Not a breach — Coolify already has root there — but it is sloppy, and it means the admin's keys drift as Coolify's do.command=/from=restrictions or forced commands in root'sauthorized_keys. Copied verbatim, those silently follow to the admin user.Open question — where does the admin key come from?
authorized_keysat creation, never re-seed. Zero new arguments, live proof the key works, one honest non-convergent step. My lean.--admin-key <pubkey>explicit. Convergent, immune to Coolify's key drift and to inheritedcommand=restrictions — but the operator can hand over a key they do not hold, which is exactly the lockout (a) rules out.(a) is safer against lockout; (b) is cleaner against drift. They can also compose: seed from root, let
--admin-keyoverride.Why this blocks #12
box'shost/setup-host.shis written for a sudo-capable non-root user: it callssudo apt-get, and if$USERis not inincus-adminit adds the group, prints "re-login and re-run", andexit 0. Run as root out ofrig bootstrap dev, that returns success having done half the job — precisely the "claimed success without checking effective state" failure the sshd first-wins bug already taught this repo once.So the
devrole needs the admin user to exist and to be the identitysetup-host.shruns under. This issue is a prerequisite for #12, not a neighbour of it.Constraints
sudo, neverdocker.Rehearsal
The Incus container rehearsal cannot see this (no cloud-init drop-in, no Coolify). The real rehearsal is a throwaway cloud box: bootstrap, confirm inbound SSH as the admin user from a second terminal — the one check that matters and the one rig cannot self-assert — then close root's door, then re-run rig and confirm it is still convergent through the admin path. Assert effective state (
sshd -T -C user=…,sudo -n true), never the files rig wrote.