forked from heavy-duty/rig
Seven review findings on the users family, each with the harness check that
would have caught it:
- Invoker gate (apply + close-root): %rig's sudoers rule is binary-scoped but
not argument-scoped, so `sudo rig users apply --file <me-as-admin>` made
role rig silently root-equivalent through the very command that granted it.
Identity management now refuses any sudo invoker outside rig-admin; direct
root (bring-up, a root shell) proceeds.
- Offboarding revokes SSH, not just the password: a '!'-locked password is
not a closed door under UsePAM — Debian sshd still honors the pubkey. A
dropped user's account is now expired (usermod -L -e 1, the switch PAM
actually enforces) and authorized_keys is renamed to
authorized_keys.revoked-by-rig — access revoked, data kept, convergence
never destroys. Present users get their expiry cleared idempotently, so a
re-added user comes back to life.
- The ledger remembers: two-field lines ('name active' / 'name revoked',
legacy bare names read as active), so dropped users no longer vanish from
rig's memory on the next rewrite. status now reports the ledger state
corroborated by the account's real expiry — passwd -S read L for everyone
(apply locks all passwords always), so its locked/active was meaningless —
and flags a mismatch loudly as drift.
- Perms are part of the converged state: ~/.ssh and authorized_keys ownership
and mode converge on every run, not only when content changes — StrictModes
treats them as load-bearing, so drifted perms were a broken login that
"already converged" lied about. Only the content write stays cmp-guarded.
- close-root's admin-door gate checks the StrictModes shape per candidate —
ownership, group/world-writability of home/.ssh/authorized_keys, a real
login shell, an unexpired account — and names which check failed. It proves
the door SHOULD open, not that it does; the separate-session advisory stays
load-bearing.
- Usernames are validated in the parser's one-pass refusal matrix
(^[a-z_][a-z0-9_-]{0,31}$): 'fo|o' corrupted the parser's own '|'-delimited
stream, and a leading '-' read as a useradd flag mid-convergence.
- The box role is trait-aware: on a host=no box an absent incus group skips
the role with a warning and converges everything else — one box-role user
in a fleet-wide file must not abort apply everywhere VMs don't live.
host=yes still dies pointing at box setup-host; a classless marker warns
toward a bootstrap re-run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
3.5 KiB
Bash
Executable file
88 lines
3.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# rig users status — what this box's operator accounts actually are, read from
|
|
# the machine itself: roles derived from REAL group membership (not the
|
|
# ledger's memory of an apply), key counts from authorized_keys, and the
|
|
# active/revoked state from the ledger CORROBORATED by the account's actual
|
|
# expiry — apply locks every password always, so the lock flag says nothing;
|
|
# expiry is the switch that actually revokes, and a mismatch between ledger
|
|
# and expiry is drift worth shouting about. Reads only — no network, no
|
|
# writes.
|
|
set -euo pipefail
|
|
|
|
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 status
|
|
|
|
Per rig-managed user (the /etc/rig/users ledger): roles derived from the
|
|
groups the user is ACTUALLY in (rig-admin -> admin, rig -> rig, incus -> box),
|
|
the authorized_keys count ('revoked' when only the .revoked-by-rig rename
|
|
remains), and whether the user is active or revoked — the ledger's word,
|
|
checked against the account's real expiry, with a loud warning when the two
|
|
disagree (a drifted box must never read as healthy). Reads the box only — no
|
|
network, no writes. Run as root (shadow is read).
|
|
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
|
|
|
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
|
|
|
LEDGER=/etc/rig/users
|
|
if [ ! -r "$LEDGER" ]; then
|
|
log "no rig-managed users (no $LEDGER yet — rig users apply creates it)"
|
|
exit 0
|
|
fi
|
|
|
|
today=$(( $(date +%s) / 86400 ))
|
|
while read -r u lstate _; do
|
|
[ -n "$u" ] || continue
|
|
# Ledger lines are 'name active' / 'name revoked'; a legacy bare name (a
|
|
# ledger written before states existed) reads as active.
|
|
[ -n "${lstate:-}" ] || lstate=active
|
|
if ! id -u "$u" >/dev/null 2>&1; then
|
|
# In the ledger but off the box: someone deleted by hand what rig only
|
|
# ever revokes. Say so rather than crash or silently skip.
|
|
warn "$u: in the ledger but not on the box (rig never deletes — removed by hand?)"
|
|
continue
|
|
fi
|
|
groups=" $(id -nG "$u") "
|
|
roles=""
|
|
case "$groups" in *" rig-admin "*) roles="admin" ;; esac
|
|
case "$groups" in *" rig "*) roles="${roles:+$roles,}rig" ;; esac
|
|
case "$groups" in *" incus "*) roles="${roles:+$roles,}box" ;; esac
|
|
[ -n "$roles" ] || roles="none"
|
|
home="$(getent passwd "$u" | cut -d: -f6)"
|
|
keys=0
|
|
if [ -r "$home/.ssh/authorized_keys" ]; then
|
|
keys="$(grep -c . "$home/.ssh/authorized_keys" || true)"
|
|
elif [ -e "$home/.ssh/authorized_keys.revoked-by-rig" ]; then
|
|
# Only the rename apply's revocation performed remains: access revoked,
|
|
# data kept.
|
|
keys=revoked
|
|
fi
|
|
# Corroborate the ledger against the switch that actually revokes: shadow
|
|
# field 8 is the expiry (days since epoch, empty = never). The ledger is
|
|
# apply's memory; the expiry is the machine's present tense — when they
|
|
# disagree, someone changed the account behind rig's back.
|
|
exp="$(getent shadow "$u" 2>/dev/null | cut -d: -f8)"
|
|
actual=active
|
|
if [ -n "$exp" ] && [ "$exp" -le "$today" ] 2>/dev/null; then
|
|
actual=revoked
|
|
fi
|
|
if [ "$actual" != "$lstate" ]; then
|
|
warn "$u: DRIFT — ledger says $lstate but the account's expiry says $actual; re-run rig users apply"
|
|
log "$u roles=$roles keys=$keys $lstate (DRIFT: expiry says $actual)"
|
|
else
|
|
log "$u roles=$roles keys=$keys $lstate"
|
|
fi
|
|
done < "$LEDGER"
|