fix(users): close-root no-op must prove the door, not the file

The clean-file fast path exited before the sshd -T assertion, so matching
bytes alone bought the 'root already closed' claim. Two ways that lies: an
earlier-sorting drop-in wins the first-wins fight while our file sits
pretty, and a prior run that died between install and restart leaves a
daemon that never read the file — sshd -T can't see that one either, since
it re-parses disk rather than interrogating the running daemon.

Now the no-op is taken only when the bytes match AND systemd says sshd
started strictly after the newest mtime across everything sshd reads (main
config, drop-in dir, drop-ins); anything less restarts behind the same
sshd -t gate, and the effective-config assertion runs on every path before
any success claim. Harness pins both: assert-before-claim ordering and the
daemon-start-vs-config-mtime proof.

Addresses PR #27 review (clean-file fast path convergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dan Claude Van Damme 2026-07-17 20:49:12 +00:00
parent 00c54a342d
commit c44a645670
2 changed files with 62 additions and 19 deletions

View file

@ -143,34 +143,64 @@ fi
DROPIN=/etc/ssh/sshd_config.d/00-rig-users.conf DROPIN=/etc/ssh/sshd_config.d/00-rig-users.conf
TMP="$(mktemp)" TMP="$(mktemp)"
printf 'PermitRootLogin no\n' > "$TMP" printf 'PermitRootLogin no\n' > "$TMP"
if cmp -s "$TMP" "$DROPIN" 2>/dev/null; then
rm -f "$TMP"
log "root already closed; nothing to do"
exit 0
fi
# Convergence needs two proofs, and matching bytes are only half of one. The
# file can be right while the DOOR is still open: a prior run that died
# between install and restart leaves a daemon that never read this file, and
# `sshd -T` cannot tell — it re-parses disk, it does not interrogate the
# running daemon. The only proof the running sshd carries this config is a
# (re)start AFTER the last change to anything sshd reads: the main config,
# the drop-in dir itself (creates/deletes/renames inside touch its mtime, so
# a since-removed override is caught), and every drop-in. So the no-op is
# taken only when the bytes match AND systemd says sshd started strictly
# after the newest of those mtimes; anything less restarts, and the
# effective-config assertion at the bottom runs on EVERY path — claiming
# "already closed" from file bytes is exactly what let bootstrap's
# first-wins bug ship green.
RESTART=1
BACKUP="" BACKUP=""
[ -e "$DROPIN" ] && { BACKUP="$(mktemp)"; cp -a "$DROPIN" "$BACKUP"; } INSTALLED=0
install -m 0644 "$TMP" "$DROPIN" if cmp -s "$TMP" "$DROPIN" 2>/dev/null; then
newest="$(stat -c '%Y' /etc/ssh/sshd_config /etc/ssh/sshd_config.d /etc/ssh/sshd_config.d/*.conf 2>/dev/null | sort -rn | head -n1)"
started="$(systemctl show ssh -p ExecMainStartTimestamp --value 2>/dev/null)" || started=""
if [ -n "$started" ] && started_s="$(date -d "$started" +%s 2>/dev/null)" \
&& [ -n "$newest" ] && [ "$started_s" -gt "$newest" ]; then
RESTART=0
fi
else
[ -e "$DROPIN" ] && { BACKUP="$(mktemp)"; cp -a "$DROPIN" "$BACKUP"; }
install -m 0644 "$TMP" "$DROPIN"
INSTALLED=1
fi
rm -f "$TMP" rm -f "$TMP"
# Validate the MERGED config BEFORE bouncing the daemon (the bootstrap shape): if [ "$RESTART" -eq 1 ]; then
# on a box whose only door is SSH — exactly what this box is about to become — # Validate the MERGED config BEFORE bouncing the daemon (the bootstrap
# restarting into a config the daemon refuses to parse leaves no listener and # shape): on a box whose only door is SSH — exactly what this box is about
# no way back in. Roll back and stop rather than shut the door on a maybe. # to become — restarting into a config the daemon refuses to parse leaves
if ! sshd -t 2>/dev/null; then # no listener and no way back in. Roll back (when we installed anything)
if [ -n "$BACKUP" ]; then cp -a "$BACKUP" "$DROPIN"; else rm -f "$DROPIN"; fi # and stop rather than shut the door on a maybe.
if ! sshd -t 2>/dev/null; then
if [ "$INSTALLED" -eq 1 ]; then
if [ -n "$BACKUP" ]; then cp -a "$BACKUP" "$DROPIN"; else rm -f "$DROPIN"; fi
rm -f "$BACKUP"
die "sshd rejects the merged config; drop-in rolled back, daemon untouched. Run 'sshd -t' to see which file is bad."
fi
die "sshd rejects the current config; daemon untouched. Run 'sshd -t' to see which file is bad."
fi
rm -f "$BACKUP" rm -f "$BACKUP"
die "sshd rejects the merged config; drop-in rolled back, daemon untouched. Run 'sshd -t' to see which file is bad." systemctl restart ssh
fi fi
rm -f "$BACKUP"
systemctl restart ssh
# Assert the EFFECTIVE config, not the file's existence — a drop-in sorting # Assert the EFFECTIVE config, not the file's existence — a drop-in sorting
# even earlier would win the first-wins fight silently. `sshd -T` is what the # even earlier would win the first-wins fight silently. `sshd -T` is what the
# daemon actually resolved. # daemon actually resolved, and it gates the no-op claim too: "already
# closed" is a statement about the door, never about the file.
eff="$(sshd -T 2>/dev/null)" || die "sshd -T failed; refusing to claim root is closed" eff="$(sshd -T 2>/dev/null)" || die "sshd -T failed; refusing to claim root is closed"
echo "$eff" | grep -qx 'permitrootlogin no' \ echo "$eff" | grep -qx 'permitrootlogin no' \
|| die "sshd still resolves permitrootlogin != no — a drop-in is beating ${DROPIN}; check ls /etc/ssh/sshd_config.d/" || die "sshd still resolves permitrootlogin != no — a drop-in is beating ${DROPIN}; check ls /etc/ssh/sshd_config.d/"
log "root door closed (sshd -T resolves permitrootlogin no); humans enter as themselves now" if [ "$RESTART" -eq 0 ]; then
log "root already closed (sshd -T resolves permitrootlogin no); nothing to do"
else
log "root door closed (sshd -T resolves permitrootlogin no); humans enter as themselves now"
fi

View file

@ -401,6 +401,19 @@ sshdt_at="$(grep -nE '^[[:space:]]*if ! sshd -t' "$ROOT/commands/users-close-roo
restart_at="$(grep -n 'systemctl restart ssh' "$ROOT/commands/users-close-root.sh" | head -n1 | cut -d: -f1)" restart_at="$(grep -n 'systemctl restart ssh' "$ROOT/commands/users-close-root.sh" | head -n1 | cut -d: -f1)"
check "users close-root: sshd -t precedes the ssh restart" \ check "users close-root: sshd -t precedes the ssh restart" \
0 "" test "${sshdt_at:-999999}" -lt "${restart_at:-0}" 0 "" test "${sshdt_at:-999999}" -lt "${restart_at:-0}"
# Convergence is a claim about the DOOR, not the file. Matching bytes can hide
# an earlier-sorting override (first-wins) or a daemon that died between
# install and restart and never read the file — so the no-op message may only
# be spoken after the effective-config assertion (`sshd -T`), and the no-op
# branch may only be TAKEN when the daemon provably started after the last
# change to sshd's config inputs. Pin both: the assert-before-claim ordering,
# and the daemon-start-vs-config-mtime proof's presence.
efft_at="$(grep -n 'sshd -T' "$ROOT/commands/users-close-root.sh" | grep -v '^[0-9]*:#' | head -n1 | cut -d: -f1)"
noop_at="$(grep -n 'nothing to do' "$ROOT/commands/users-close-root.sh" | tail -n1 | cut -d: -f1)"
check "users close-root: no-op claim sits after the effective-config assert" \
0 "" test "${efft_at:-999999}" -lt "${noop_at:-0}"
check "users close-root: no-op needs a daemon start newer than the config" 0 "" \
grep -q "ExecMainStartTimestamp" "$ROOT/commands/users-close-root.sh"
# The admin-door gate must check the StrictModes SHAPE, not file existence: a # The admin-door gate must check the StrictModes SHAPE, not file existence: a
# non-empty authorized_keys behind group/world-writable perms is a key sshd # non-empty authorized_keys behind group/world-writable perms is a key sshd
# rejects — closing root behind it welds the only door shut. The full gate # rejects — closing root behind it welds the only door shut. The full gate