Fix: Complete SSH hardening recovery

This commit is contained in:
Gabriel Brown
2026-08-27 04:06:54 -04:00
parent d267da58ad
commit 51e2c8418a
2 changed files with 300 additions and 27 deletions
+45 -12
View File
@@ -84,15 +84,15 @@ restore_ssh_dropin() {
if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then
restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1 restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1
if ! cp -a -- "$ssh_backup" "$restore"; then if ! cp -a -- "$ssh_backup" "$restore"; then
rm -f -- "$restore" remove_ssh_artifact "$restore" || true
return 1 return 1
fi fi
if ! mv -f -- "$restore" "$ssh_dropin"; then if ! mv -f -- "$restore" "$ssh_dropin"; then
rm -f -- "$restore" remove_ssh_artifact "$restore" || true
return 1 return 1
fi fi
else else
rm -f -- "$ssh_dropin" remove_ssh_artifact "$ssh_dropin"
fi fi
} }
@@ -104,6 +104,37 @@ restore_ssh_transaction_traps() {
return 0 return 0
} }
remove_ssh_artifact() {
local artifact="$1"
[[ -n "$artifact" && -e "$artifact" ]] || return 0
if rm -f -- "$artifact"; then
return 0
fi
printf 'SSH transaction cleanup failed. Retained artifact: %s\n' "$artifact" >&2
printf ' rm -f -- %q\n' "$artifact" >&2
return 1
}
handle_ssh_transaction_signal() {
local signal_status="$1"
trap - INT TERM
rollback_failed=0
restore_ssh_dropin || rollback_failed=1
sshd -t || rollback_failed=1
systemctl reload "$ssh_unit" || rollback_failed=1
ssh_transaction_active=0
restore_ssh_transaction_traps
if (( rollback_failed )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
else
remove_ssh_artifact "$ssh_backup" || true
fi
exit "$signal_status"
}
harden_server_ssh() { harden_server_ssh() {
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
local ssh_candidate="" ssh_backup="" rollback_failed=0 local ssh_candidate="" ssh_backup="" rollback_failed=0
@@ -125,17 +156,18 @@ harden_server_ssh() {
ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.tmp)" || return 1 ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.tmp)" || return 1
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$ssh_candidate"; then if ! printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$ssh_candidate"; then
rm -f -- "$ssh_candidate" remove_ssh_artifact "$ssh_candidate" || true
return 1 return 1
fi fi
if [[ -e "$ssh_dropin" ]]; then if [[ -e "$ssh_dropin" ]]; then
ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.backup)" || { ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.backup)" || {
rm -f -- "$ssh_candidate" remove_ssh_artifact "$ssh_candidate" || true
return 1 return 1
} }
if ! cat -- "$ssh_dropin" >"$ssh_backup"; then if ! cat -- "$ssh_dropin" >"$ssh_backup"; then
rm -f -- "$ssh_candidate" "$ssh_backup" remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
return 1 return 1
fi fi
fi fi
@@ -144,14 +176,15 @@ harden_server_ssh() {
ssh_saved_int_trap="$(trap -p INT)" ssh_saved_int_trap="$(trap -p INT)"
ssh_saved_term_trap="$(trap -p TERM)" ssh_saved_term_trap="$(trap -p TERM)"
trap 'if [[ "${ssh_transaction_active:-0}" == 1 ]]; then restore_ssh_dropin || true; fi' EXIT trap 'if [[ "${ssh_transaction_active:-0}" == 1 ]]; then restore_ssh_dropin || true; fi' EXIT
trap 'exit 130' INT trap 'handle_ssh_transaction_signal 130' INT
trap 'exit 143' TERM trap 'handle_ssh_transaction_signal 143' TERM
ssh_transaction_active=1 ssh_transaction_active=1
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
ssh_transaction_active=0 ssh_transaction_active=0
restore_ssh_transaction_traps restore_ssh_transaction_traps
rm -f -- "$ssh_candidate" "$ssh_backup" remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
return 1 return 1
fi fi
ssh_candidate="" ssh_candidate=""
@@ -167,7 +200,7 @@ harden_server_ssh() {
printf ' sshd -t\n' >&2 printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2 printf ' systemctl reload %s\n' "$ssh_unit" >&2
else else
[[ -z "$ssh_backup" ]] || rm -f -- "$ssh_backup" remove_ssh_artifact "$ssh_backup" || true
fi fi
return 1 return 1
fi fi
@@ -184,14 +217,14 @@ harden_server_ssh() {
printf ' sshd -t\n' >&2 printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2 printf ' systemctl reload %s\n' "$ssh_unit" >&2
else else
[[ -z "$ssh_backup" ]] || rm -f -- "$ssh_backup" remove_ssh_artifact "$ssh_backup" || true
fi fi
return 1 return 1
fi fi
ssh_transaction_active=0 ssh_transaction_active=0
restore_ssh_transaction_traps restore_ssh_transaction_traps
[[ -z "$ssh_backup" ]] || rm -f -- "$ssh_backup" remove_ssh_artifact "$ssh_backup" || return 1
echo "Wrote $ssh_dropin; make sure your key works before logging out." echo "Wrote $ssh_dropin; make sure your key works before logging out."
} }
+255 -15
View File
@@ -21,11 +21,13 @@ import fcntl
import os import os
import pty import pty
import re import re
import signal
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import termios import termios
import time
from pathlib import Path from pathlib import Path
boot = sys.argv[1] boot = sys.argv[1]
@@ -142,7 +144,32 @@ esac
''') ''')
write_executable(stub_dir / "mv", common + r''' write_executable(stub_dir / "mv", common + r'''
log mv "$@" "source-mode=$(/usr/bin/stat -c %a -- "${3:-}")" log mv "$@" "source-mode=$(/usr/bin/stat -c %a -- "${3:-}")"
exec /usr/bin/mv "$@" if [[ "${3:-}" == *.tmp ]]; then
consume_result MV_ACTIVATION_RESULTS
result=$?
(( result == 0 )) || exit "$result"
fi
/usr/bin/mv "$@"
if [[ "${3:-}" == *.tmp && -e "$PANAMA_BOOT_FIXTURE_ROOT/state/HOLD_ACTIVATION" ]]; then
: >"$PANAMA_BOOT_FIXTURE_ROOT/state/ACTIVATED"
while [[ ! -e "$PANAMA_BOOT_FIXTURE_ROOT/state/RELEASE_ACTIVATION" ]]; do
/usr/bin/sleep 0.01
done
fi
''')
write_executable(stub_dir / "rm", common + r'''
log rm "$@"
if [[ "${1:-}" == -f && "${2:-}" == -- && "${3:-}" == *.backup ]]; then
consume_result RM_BACKUP_RESULTS
result=$?
(( result == 0 )) || exit "$result"
fi
if [[ "${1:-}" == -f && "${2:-}" == -- && "${3:-}" == *.tmp ]]; then
consume_result RM_CANDIDATE_RESULTS
result=$?
(( result == 0 )) || exit "$result"
fi
exec /usr/bin/rm "$@"
''') ''')
for command in ("useradd", "usermod"): for command in ("useradd", "usermod"):
write_executable(stub_dir / command, common + f'''\nlog {command} "$@"\nexit 97\n''') write_executable(stub_dir / command, common + f'''\nlog {command} "$@"\nexit 97\n''')
@@ -153,9 +180,13 @@ def configure_case(
*, *,
sshd_results: tuple[int, ...] = (), sshd_results: tuple[int, ...] = (),
reload_results: tuple[int, ...] = (), reload_results: tuple[int, ...] = (),
mv_activation_results: tuple[int, ...] = (),
rm_backup_results: tuple[int, ...] = (),
rm_candidate_results: tuple[int, ...] = (),
prior_dropin: bytes | None = None, prior_dropin: bytes | None = None,
sshd_unit: bool = True, sshd_unit: bool = True,
ssh_unit: bool = True, ssh_unit: bool = True,
hold_activation: bool = False,
) -> tuple[Path, Path]: ) -> tuple[Path, Path]:
fixture_root = work / name / "root" fixture_root = work / name / "root"
stub_dir = work / name / "bin" stub_dir = work / name / "bin"
@@ -176,8 +207,19 @@ def configure_case(
(state / "root-key-meta").write_text("0:600\n") (state / "root-key-meta").write_text("0:600\n")
(state / "SSHD_RESULTS").write_text("".join(f"{result}\n" for result in sshd_results)) (state / "SSHD_RESULTS").write_text("".join(f"{result}\n" for result in sshd_results))
(state / "RELOAD_RESULTS").write_text("".join(f"{result}\n" for result in reload_results)) (state / "RELOAD_RESULTS").write_text("".join(f"{result}\n" for result in reload_results))
(state / "MV_ACTIVATION_RESULTS").write_text(
"".join(f"{result}\n" for result in mv_activation_results)
)
(state / "RM_BACKUP_RESULTS").write_text(
"".join(f"{result}\n" for result in rm_backup_results)
)
(state / "RM_CANDIDATE_RESULTS").write_text(
"".join(f"{result}\n" for result in rm_candidate_results)
)
(state / "SSHD_UNIT").write_text("present\n" if sshd_unit else "absent\n") (state / "SSHD_UNIT").write_text("present\n" if sshd_unit else "absent\n")
(state / "SSH_UNIT").write_text("present\n" if ssh_unit else "absent\n") (state / "SSH_UNIT").write_text("present\n" if ssh_unit else "absent\n")
if hold_activation:
(state / "HOLD_ACTIVATION").touch()
(fixture_root / "stub-install").write_text( (fixture_root / "stub-install").write_text(
"#!/usr/bin/env bash\nprintf 'install-handoff %s\\n' \"${PANAMA_PATH:-unset}\" >> \"$PANAMA_BOOT_FIXTURE_ROOT/calls\"\n" "#!/usr/bin/env bash\nprintf 'install-handoff %s\\n' \"${PANAMA_PATH:-unset}\" >> \"$PANAMA_BOOT_FIXTURE_ROOT/calls\"\n"
) )
@@ -232,9 +274,16 @@ def configure_case(
"success-without-prior-dropin", "success-without-prior-dropin",
"success-replaces-prior-dropin", "success-replaces-prior-dropin",
"candidate-invalid", "candidate-invalid",
"candidate-invalid-without-prior",
"candidate-reload-fails", "candidate-reload-fails",
"candidate-reload-fails-without-prior",
"rollback-validation-fails", "rollback-validation-fails",
"rollback-reload-fails", "rollback-reload-fails",
"success-backup-cleanup-fails",
"rollback-backup-cleanup-fails",
"signal-int-restores-prior",
"signal-term-removes-new-dropin",
"candidate-cleanup-fails",
): ):
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text("ssh-ed25519 target\n")
elif name == "safe-root-key-copy": elif name == "safe-root-key-copy":
@@ -244,11 +293,25 @@ def configure_case(
return fixture_root, stub_dir return fixture_root, stub_dir
def run_case(name: str, **configuration: object) -> tuple[int, str, str, Path]: def run_case(
fixture_root, stub_dir = configure_case(name, **configuration) name: str,
*,
signal_after_activation: int | None = None,
prior_traps: bool = False,
**configuration: object,
) -> tuple[int, str, str, Path, int]:
fixture_root, stub_dir = configure_case(
name,
hold_activation=signal_after_activation is not None,
**configuration,
)
master, slave = pty.openpty() master, slave = pty.openpty()
def attach_terminal() -> None: def attach_terminal() -> None:
# The test runner launches contracts as background jobs, which inherit
# SIGINT ignored. A real interactive bootstrap starts with SIGINT at
# its default disposition, so restore that state before exec.
signal.signal(signal.SIGINT, signal.SIG_DFL)
fcntl.ioctl(0, termios.TIOCSCTTY, 0) fcntl.ioctl(0, termios.TIOCSCTTY, 0)
env = { env = {
@@ -258,6 +321,18 @@ def run_case(name: str, **configuration: object) -> tuple[int, str, str, Path]:
"PANAMA_PATH": f"{fixture_root}/home/gib/.local/share/Panama", "PANAMA_PATH": f"{fixture_root}/home/gib/.local/share/Panama",
"HOME": f"{fixture_root}/root", "HOME": f"{fixture_root}/root",
} }
if prior_traps:
bash_env = fixture_root / "prior-traps"
bash_env.write_text(
'''if [[ "$0" == "$PANAMA_BOOT_SCRIPT" ]]; then
trap 'printf "prior-exit %s\\n" "$BASHPID" >>"$PANAMA_BOOT_FIXTURE_ROOT/calls"' EXIT
trap 'printf "prior-int %s\\n" "$BASHPID" >>"$PANAMA_BOOT_FIXTURE_ROOT/calls"' INT
trap 'printf "prior-term %s\\n" "$BASHPID" >>"$PANAMA_BOOT_FIXTURE_ROOT/calls"' TERM
fi
'''
)
env["BASH_ENV"] = str(bash_env)
env["PANAMA_BOOT_SCRIPT"] = boot
process = subprocess.Popen( process = subprocess.Popen(
["bash", boot, "--server"], ["bash", boot, "--server"],
stdin=slave, stdin=slave,
@@ -269,6 +344,16 @@ def run_case(name: str, **configuration: object) -> tuple[int, str, str, Path]:
) )
os.close(slave) os.close(slave)
os.write(master, b"gib\nY\n") os.write(master, b"gib\nY\n")
if signal_after_activation is not None:
activation = fixture_root / "state/ACTIVATED"
deadline = time.monotonic() + 5
while not activation.exists() and process.poll() is None and time.monotonic() < deadline:
time.sleep(0.01)
if not activation.exists():
note(f"{name}: fixture did not observe atomic activation before signaling")
else:
os.kill(process.pid, signal_after_activation)
(fixture_root / "state/RELEASE_ACTIVATION").touch()
chunks: list[bytes] = [] chunks: list[bytes] = []
while True: while True:
try: try:
@@ -284,7 +369,7 @@ def run_case(name: str, **configuration: object) -> tuple[int, str, str, Path]:
status = process.wait() status = process.wait()
calls = (fixture_root / "calls").read_text() calls = (fixture_root / "calls").read_text()
output = b"".join(chunks).decode(errors="replace") output = b"".join(chunks).decode(errors="replace")
return status, output, calls, fixture_root return status, output, calls, fixture_root, process.pid
unsafe_cases = ( unsafe_cases = (
@@ -302,7 +387,7 @@ unsafe_cases = (
"relative-home", "relative-home",
) )
for case in unsafe_cases: for case in unsafe_cases:
status, output, calls, fixture_root = run_case(case) status, output, calls, fixture_root, _ = run_case(case)
if status != 0: if status != 0:
note(f"{case}: bootstrap stopped with status {status}: {output.strip()}") note(f"{case}: bootstrap stopped with status {status}: {output.strip()}")
if "SSH hardening unavailable" not in output: if "SSH hardening unavailable" not in output:
@@ -321,7 +406,7 @@ for case in unsafe_cases:
note(f"{case}: unsafe login path did not hand off to install") note(f"{case}: unsafe login path did not hand off to install")
for case in ("safe-existing-key", "safe-root-key-copy"): for case in ("safe-existing-key", "safe-root-key-copy"):
status, output, calls, fixture_root = run_case(case) status, output, calls, fixture_root, _ = run_case(case)
if status != 0: if status != 0:
note(f"{case}: safe login path stopped with status {status}: {output.strip()}") note(f"{case}: safe login path stopped with status {status}: {output.strip()}")
if "SSH hardening unavailable" in output: if "SSH hardening unavailable" in output:
@@ -365,6 +450,14 @@ transaction_cases = {
"ssh_unit": True, "ssh_unit": True,
"succeeds": False, "succeeds": False,
}, },
"candidate-invalid-without-prior": {
"sshd_results": (1, 0),
"reload_results": (),
"prior_dropin": None,
"sshd_unit": True,
"ssh_unit": True,
"succeeds": False,
},
"candidate-reload-fails": { "candidate-reload-fails": {
"sshd_results": (0, 0), "sshd_results": (0, 0),
"reload_results": (1, 0), "reload_results": (1, 0),
@@ -373,6 +466,14 @@ transaction_cases = {
"ssh_unit": True, "ssh_unit": True,
"succeeds": False, "succeeds": False,
}, },
"candidate-reload-fails-without-prior": {
"sshd_results": (0, 0),
"reload_results": (1, 0),
"prior_dropin": None,
"sshd_unit": True,
"ssh_unit": True,
"succeeds": False,
},
"rollback-validation-fails": { "rollback-validation-fails": {
"sshd_results": (0, 1), "sshd_results": (0, 1),
"reload_results": (1,), "reload_results": (1,),
@@ -399,7 +500,7 @@ for case, expected in transaction_cases.items():
for key, value in expected.items() for key, value in expected.items()
if key not in {"succeeds", "rollback_fails"} if key not in {"succeeds", "rollback_fails"}
} }
status, output, calls, fixture_root = run_case(case, **configuration) status, output, calls, fixture_root, _ = run_case(case, **configuration)
call_lines = calls.splitlines() call_lines = calls.splitlines()
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf"
sshd_dir = dropin.parent sshd_dir = dropin.parent
@@ -427,6 +528,12 @@ for case, expected in transaction_cases.items():
line, line,
) )
] ]
removal_lines = [
index
for index, line in enumerate(call_lines)
if line == f"rm -f -- {dropin} "
]
rollback_lines = restore_lines if expected["prior_dropin"] is not None else removal_lines
succeeds = bool(expected["succeeds"]) succeeds = bool(expected["succeeds"])
if succeeds and status != 0: if succeeds and status != 0:
@@ -438,7 +545,7 @@ for case, expected in transaction_cases.items():
if not succeeds and "install-handoff " in calls: if not succeeds and "install-handoff " in calls:
note(f"{case}: failed transaction handed off to install") note(f"{case}: failed transaction handed off to install")
wanted_contents = desired_dropin if succeeds else prior_dropin wanted_contents = desired_dropin if succeeds else expected["prior_dropin"]
actual_contents = dropin.read_bytes() if dropin.exists() else None actual_contents = dropin.read_bytes() if dropin.exists() else None
if actual_contents != wanted_contents: if actual_contents != wanted_contents:
note(f"{case}: SSH drop-in contents were not {'activated' if succeeds else 'restored'}") note(f"{case}: SSH drop-in contents were not {'activated' if succeeds else 'restored'}")
@@ -450,21 +557,21 @@ for case, expected in transaction_cases.items():
note(f"{case}: success did not validate once and reload once") note(f"{case}: success did not validate once and reload once")
elif activation_lines and not activation_lines[0][0] < validations[0] < reloads[0]: elif activation_lines and not activation_lines[0][0] < validations[0] < reloads[0]:
note(f"{case}: success did not activate, validate, then reload") note(f"{case}: success did not activate, validate, then reload")
elif case == "candidate-invalid": elif case in {"candidate-invalid", "candidate-invalid-without-prior"}:
if len(validations) != 2 or reloads: if len(validations) != 2 or reloads:
note(f"{case}: invalid candidate did not validate candidate and restoration without reload") note(f"{case}: invalid candidate did not validate candidate and restoration without reload")
elif activation_lines and restore_lines and not ( elif activation_lines and rollback_lines and not (
activation_lines[0][0] < validations[0] < restore_lines[0] < validations[1] activation_lines[0][0] < validations[0] < rollback_lines[0] < validations[1]
): ):
note(f"{case}: rollback command order was wrong") note(f"{case}: rollback command order was wrong")
else: else:
if len(validations) != 2 or len(reloads) != 2: if len(validations) != 2 or len(reloads) != 2:
note(f"{case}: reload failure did not validate and reload the restored configuration") note(f"{case}: reload failure did not validate and reload the restored configuration")
elif activation_lines and restore_lines and not ( elif activation_lines and rollback_lines and not (
activation_lines[0][0] activation_lines[0][0]
< validations[0] < validations[0]
< reloads[0] < reloads[0]
< restore_lines[0] < rollback_lines[0]
< validations[1] < validations[1]
< reloads[1] < reloads[1]
): ):
@@ -472,8 +579,9 @@ for case, expected in transaction_cases.items():
if succeeds and restore_lines: if succeeds and restore_lines:
note(f"{case}: successful transaction performed a rollback") note(f"{case}: successful transaction performed a rollback")
if not succeeds and len(restore_lines) != 1: if not succeeds:
note(f"{case}: prior drop-in was not restored exactly once") if len(rollback_lines) != 1:
note(f"{case}: pre-transaction SSH state was not restored exactly once")
detected_unit = "ssh.service" if case == "success-replaces-prior-dropin" else "sshd.service" detected_unit = "ssh.service" if case == "success-replaces-prior-dropin" else "sshd.service"
other_unit = "sshd.service" if detected_unit == "ssh.service" else "ssh.service" other_unit = "sshd.service" if detected_unit == "ssh.service" else "ssh.service"
@@ -524,6 +632,138 @@ for case, expected in transaction_cases.items():
): ):
note(f"{case}: backup was not collision-safe, same-directory, non-.conf, and restrictive") note(f"{case}: backup was not collision-safe, same-directory, non-.conf, and restrictive")
cleanup_failure_cases = {
"success-backup-cleanup-fails": {
"sshd_results": (0,),
"reload_results": (0,),
"rm_backup_results": (1,),
"expected_dropin": desired_dropin,
"expected_validations": 1,
"expected_reloads": 1,
},
"rollback-backup-cleanup-fails": {
"sshd_results": (1, 0),
"reload_results": (),
"rm_backup_results": (1,),
"expected_dropin": prior_dropin,
"expected_validations": 2,
"expected_reloads": 0,
},
}
for case, expected in cleanup_failure_cases.items():
status, output, calls, fixture_root, _ = run_case(
case,
sshd_results=expected["sshd_results"],
reload_results=expected["reload_results"],
rm_backup_results=expected["rm_backup_results"],
prior_dropin=prior_dropin,
)
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf"
backups = list(dropin.parent.glob(".90-panama.*.backup"))
if status == 0:
note(f"{case}: cleanup failure returned success")
if "install-handoff " in calls:
note(f"{case}: cleanup failure handed off to install")
if dropin.read_bytes() != expected["expected_dropin"]:
note(f"{case}: cleanup failure changed the settled SSH drop-in")
if calls.count("sshd -t \n") != expected["expected_validations"]:
note(f"{case}: cleanup failure validation count was wrong")
if calls.count("systemctl reload sshd.service \n") != expected["expected_reloads"]:
note(f"{case}: cleanup failure reload count was wrong")
if len(backups) != 1:
note(f"{case}: failed cleanup did not retain exactly one backup")
else:
backup = backups[0]
if str(backup.resolve()) not in output or "rm -f --" not in output:
note(f"{case}: retained backup was not reported with an actionable cleanup command")
status, output, calls, fixture_root, _ = run_case(
"candidate-cleanup-fails",
mv_activation_results=(1,),
rm_candidate_results=(1,),
)
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf"
candidates = list(dropin.parent.glob(".90-panama.*.tmp"))
if status == 0:
note("candidate-cleanup-fails: activation cleanup failure returned success")
if dropin.exists():
note("candidate-cleanup-fails: failed activation changed the final drop-in")
if "install-handoff " in calls:
note("candidate-cleanup-fails: failed activation reached install handoff")
if len(candidates) != 1:
note("candidate-cleanup-fails: failed cleanup did not retain exactly one candidate")
else:
candidate = candidates[0]
if str(candidate.resolve()) not in output or "rm -f --" not in output:
note("candidate-cleanup-fails: retained candidate lacked an actionable cleanup command")
signal_cases = {
"signal-int-restores-prior": {
"signal": signal.SIGINT,
"status": 130,
"prior_dropin": prior_dropin,
},
"signal-term-removes-new-dropin": {
"signal": signal.SIGTERM,
"status": 143,
"prior_dropin": None,
},
}
for case, expected in signal_cases.items():
status, output, calls, fixture_root, boot_pid = run_case(
case,
signal_after_activation=expected["signal"],
prior_traps=True,
sshd_results=(0,),
reload_results=(0,),
prior_dropin=expected["prior_dropin"],
)
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf"
actual_dropin = dropin.read_bytes() if dropin.exists() else None
if status != expected["status"]:
note(f"{case}: signal returned status {status}, expected {expected['status']}")
if actual_dropin != expected["prior_dropin"]:
note(f"{case}: signal did not restore the pre-transaction SSH state")
if list(dropin.parent.glob(".90-panama.*")):
note(f"{case}: signal left transaction residue")
if "install-handoff " in calls:
note(f"{case}: signal reached install handoff")
if f"prior-exit {boot_pid}\n" not in calls:
note(f"{case}: signal suppressed the saved EXIT trap")
if calls.count("sshd -t \n") != 1:
note(f"{case}: signal did not validate restored configuration once")
if calls.count("systemctl reload sshd.service \n") != 1:
note(f"{case}: signal did not reload restored configuration once")
call_lines = calls.splitlines()
rollback_indices = [
index
for index, line in enumerate(call_lines)
if (
expected["prior_dropin"] is not None
and re.fullmatch(
rf"mv -f -- {re.escape(str(dropin.parent))}/\.90-panama\.[A-Za-z0-9]+\.restore "
rf"{re.escape(str(dropin))} source-mode=600 ",
line,
)
)
or (expected["prior_dropin"] is None and line == f"rm -f -- {dropin} ")
]
validation_indices = [
index for index, line in enumerate(call_lines) if line == "sshd -t "
]
reload_indices = [
index
for index, line in enumerate(call_lines)
if line == "systemctl reload sshd.service "
]
if not (
len(rollback_indices) == len(validation_indices) == len(reload_indices) == 1
and rollback_indices[0] < validation_indices[0] < reload_indices[0]
):
note(f"{case}: signal did not restore, validate, then reload in order")
if findings: if findings:
print(f"root server bootstrap: {len(findings)} finding(s)", file=sys.stderr) print(f"root server bootstrap: {len(findings)} finding(s)", file=sys.stderr)
for finding in findings: for finding in findings: