diff --git a/commands/users-close-root.sh b/commands/users-close-root.sh index 093ebe5..26d8d74 100755 --- a/commands/users-close-root.sh +++ b/commands/users-close-root.sh @@ -143,34 +143,64 @@ fi DROPIN=/etc/ssh/sshd_config.d/00-rig-users.conf TMP="$(mktemp)" 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="" -[ -e "$DROPIN" ] && { BACKUP="$(mktemp)"; cp -a "$DROPIN" "$BACKUP"; } -install -m 0644 "$TMP" "$DROPIN" +INSTALLED=0 +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" -# Validate the MERGED config BEFORE bouncing the daemon (the bootstrap shape): -# on a box whose only door is SSH — exactly what this box is about to become — -# restarting into a config the daemon refuses to parse leaves no listener and -# no way back in. Roll back and stop rather than shut the door on a maybe. -if ! sshd -t 2>/dev/null; then - if [ -n "$BACKUP" ]; then cp -a "$BACKUP" "$DROPIN"; else rm -f "$DROPIN"; fi +if [ "$RESTART" -eq 1 ]; then + # Validate the MERGED config BEFORE bouncing the daemon (the bootstrap + # shape): on a box whose only door is SSH — exactly what this box is about + # to become — restarting into a config the daemon refuses to parse leaves + # no listener and no way back in. Roll back (when we installed anything) + # 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" - 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 -rm -f "$BACKUP" - -systemctl restart ssh # 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 -# 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" 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/" -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 diff --git a/test/cli.sh b/test/cli.sh index 45679d5..a1cf8a5 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -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)" check "users close-root: sshd -t precedes the ssh restart" \ 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 # 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