From d1b6fec5f8700cfb1e47eee92a33aaf7dd7211e2 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 14:53:50 +0000 Subject: [PATCH] =?UTF-8?q?feat(users):=20close-root=20proves=20the=20door?= =?UTF-8?q?=20opens,=20not=20that=20it=20should=20=E2=80=94=20sudo=20-n=20?= =?UTF-8?q?and=20per-user=20sshd=20-T=20join=20the=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The StrictModes-shaped gate reads files, and files can all look right while the door stays shut: a sudoers drop-in that never landed, an AllowUsers or Match block elsewhere in sshd's config. #17 names the two checks that interrogate behavior instead, and they now run per candidate, additively, before the drop-in installs: 'runuser -u -- sudo -n true' (NOPASSWD sudo answers or it does not — -n never prompts; a missing runuser skips the proof with a loud warning rather than blocking the door on a missing prover), and 'sshd -T -C user=,host=...,addr=...' (the per-user EFFECTIVE config — pubkeyauthentication yes, no literal DenyUsers hit, AllowUsers if set must name them; Allow/Deny patterns match literally, fail closed). The one thing no local check can prove remains possession of the private key — the separate-session advisory stays load-bearing. Co-Authored-By: Claude Fable 5 --- commands/users-close-root.sh | 72 +++++++++++++++++++++++++++++++----- test/cli.sh | 28 ++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/commands/users-close-root.sh b/commands/users-close-root.sh index 26d8d74..09846d4 100755 --- a/commands/users-close-root.sh +++ b/commands/users-close-root.sh @@ -12,6 +12,7 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" . "$HERE/lib/users-config.sh" log() { printf 'rig-users: %s\n' "$*"; } +warn() { printf 'rig-users: WARNING: %s\n' "$*" >&2; } die() { printf 'rig-users: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { @@ -26,10 +27,14 @@ Human class ONLY. On class=server, root SSH is the control plane's (Coolify's) automation identity — closing it severs fleet management — so close-root refuses there, with no --force. It also refuses without a role marker (re-run rig bootstrap; never shut the root door blind) and refuses while no rig-admin -member holds a login sshd would plausibly accept — authorized_keys present -and non-empty, home/.ssh/keys owned by the user and not group/world-writable -(sshd's StrictModes rejects the key otherwise), a real login shell, account -not expired. The refusal names which check failed, per candidate. Run rig +member holds a login this box would actually honor. Per candidate, in order: +the StrictModes shape (authorized_keys present and non-empty, home/.ssh/keys +owned by the user and not group/world-writable, a real login shell, account +not expired), then two reachability proofs (#17) — `sudo -n true` under +runuser must answer (NOPASSWD sudo is effective, not merely written), and +`sshd -T -C user=...` must resolve a per-user effective config that accepts +the login (pubkeyauthentication yes, no DenyUsers hit, AllowUsers — if set — +names them). The refusal names which check failed, per candidate. Run rig users apply first; never close the only door. Before running, verify your admin login in a SEPARATE session — `ssh @@ -70,16 +75,33 @@ if ! WHY="$(assert_marker_human "${RIG_ROLE_MARKER:-/etc/rig/role}")"; then fi # Admin-door gate — never close the only door. Root SSH goes away below, so -# at least one rig-admin member must hold a login sshd would plausibly ACCEPT -# — a non-empty authorized_keys alone proves a file exists, not a door: +# at least one rig-admin member must hold a login this box would actually +# HONOR — a non-empty authorized_keys alone proves a file exists, not a door: # StrictModes rejects keys behind wrongly-owned or group/world-writable # paths, a nologin shell never logs in, and an expired account fails PAM # before the key is read. So every candidate is checked for the StrictModes -# shape, and the refusal names, per candidate, WHICH check failed — an -# operator staring at a refusal must see the repair. Honestly: this proves -# the door SHOULD open per StrictModes, not that it does — the -# verify-in-a-separate-session advisory in --help stays load-bearing. +# shape, and then for REACHABILITY (#17): the shape checks prove the door +# SHOULD open, these prove what can be proven from inside — that NOPASSWD +# sudo actually answers (`sudo -n true` under runuser; a sudoers drop-in +# that never landed is a shape the file checks cannot see), and that sshd's +# per-user EFFECTIVE config would accept the login (`sshd -T -C user=...` — +# an AllowUsers or Match block elsewhere can quietly exclude the admin while +# every file looks right). The refusal names, per candidate, WHICH check +# failed — an operator staring at a refusal must see the repair. Honestly: +# the one thing no local check can prove is that the operator HOLDS the +# private key — the verify-in-a-separate-session advisory in --help stays +# load-bearing. today=$(( $(date +%s) / 86400 )) +# runuser ships in util-linux on Debian — rig's target — but the gate must +# not die on a box without it: skip the live sudo proof with a loud warning +# rather than block close-root on a missing prover. Warned once, not per +# candidate. +HAVE_RUNUSER=0 +if command -v runuser >/dev/null 2>&1; then + HAVE_RUNUSER=1 +else + warn "runuser not found; skipping the live NOPASSWD-sudo proof — verify 'sudo -n true' as your admin by hand before trusting the closed door" +fi # path_strict