#!/usr/bin/env bash # rig users close-root — shut the human-class root SSH door, once and only # once a named admin can already get in. class decides root SSH's fate (#26): # on class=human a root login is unattributable noise, so it goes; on # class=server root IS the control plane's automation identity, so closing it # would sever fleet management — this command refuses there, and no --force # exists. Convergent: a second run is a no-op and says so. set -euo pipefail HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" # shellcheck source=SCRIPTDIR/lib/users-config.sh . "$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() { cat <<'EOF' usage: rig users close-root Shuts the root SSH door: installs /etc/ssh/sshd_config.d/00-rig-users.conf carrying exactly `PermitRootLogin no`, which beats bootstrap's drop-in by first-wins include order. 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 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 — where any pattern or host-qualified Deny entry counts as a hit, fail closed — AllowUsers, if set, names them literally, and the same pair of rules for DenyGroups/AllowGroups judged against the admin's actual groups from id -Gn). 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 @` while this one stays open. Root SSH is the door being welded shut; the admin door must be proven, not presumed. This is not ceremony: the local probe resolves Match blocks against a synthetic loopback client (addr=127.0.0.1), so a `Match Address` rule that treats real inbound clients differently is invisible to it — only a real login proves the real door. Run as root. Convergent: once root is closed, a re-run is a clean no-op. EOF } # --- args (validated before the root check, so errors are testable) --------- while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; *) die "unknown flag: $1" 2 ;; esac done # --- guards ------------------------------------------------------------------ [ "$(id -u)" -eq 0 ] || die "must run as root" # Identity management gates its INVOKER, not just its uid: %rig's sudoers rule # is binary-scoped but not argument-scoped, so without this gate a rig-role # user could reshape who enters this box as whom — the scoped grant silently # root-equivalent through the users family. Direct root (no SUDO_USER: # bring-up, a root shell) proceeds. if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] \ && ! id -nG "$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx rig-admin; then die "the users family changes who holds root — only rig-admin members (or root itself) may run it; role rig grants operational rig use, not identity management (invoker: $SUDO_USER)" fi # Marker gate — the policy lives in assert_marker_human (lib) so the harness # can prove its refusals against fixture markers as non-root; RIG_ROLE_MARKER # exists for the same reason: it keeps the command's own gate pointable at # fixtures instead of only at the real /etc/rig/role. if ! WHY="$(assert_marker_human "${RIG_ROLE_MARKER:-/etc/rig/role}")"; then die "$WHY" 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 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 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