Files
omarchycn/test/shell.d/sshd-hardening-migration-test.sh
T
Ryan HughesandClaude Fable 5 986962bb64 Keep the sshd hardening migration from locking users out
Validate authorized_keys line by line with the question sshd actually
asks: ssh-keygen -lf on the whole file also fingerprints a private key
copied there by mistake, which sshd cannot use, so the migration would
have disabled the only working login path.

Tighten ~/.ssh and authorized_keys the way omarchy-setup-security-sshd
does, and back off from a group-writable home directory: StrictModes
makes sshd ignore the key either way, with the same lockout.

Complete with a notice instead of failing on conditions the migration
cannot repair (a broken or pre-Include sshd_config, an overriding admin
rule, a failed reload of a valid config), so those machines keep passwords
as they were without blocking every migration queued behind this one.
Only missing privileges stay pending, since a terminal rerun fixes that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:36:26 -04:00

177 lines
8.8 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
migration="$ROOT/migrations/1788124236.sh"
stub_bin="$test_dir/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/systemctl" <<'STUB'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}"
case "$1 $2" in
"is-enabled --quiet") [[ ${SSHD_ENABLED:-0} == 1 ]] ;;
"is-active --quiet") [[ ${SSHD_ACTIVE:-0} == 1 ]] ;;
"reload sshd.service") [[ ${SSHD_RELOAD_VALID:-1} == 1 ]] ;;
*) exit 2 ;;
esac
STUB
cat >"$stub_bin/sshd" <<'STUB'
#!/bin/bash
printf 'sshd %s\n' "$*" >>"${CALL_LOG:?}"
case $1 in
-t) [[ ${SSHD_SYNTAX_VALID:-1} == 1 ]] ;;
-T)
printf 'PasswordAuthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
printf 'KbdInteractiveAuthentication %s\n' "${SSHD_KBD_AUTH:-no}"
;;
*) exit 2 ;;
esac
STUB
cat >"$stub_bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"${CALL_LOG:?}"
if [[ ${SUDO_ALLOWED:-1} != 1 ]]; then
exit 1
fi
exec "$@"
STUB
chmod +x "$stub_bin"/*
ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key"
public_key=$(<"$test_dir/key.pub")
run_migration() {
local scenario=$1
local home="$test_dir/$scenario/home"
local root="$test_dir/$scenario/root"
local config="$root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
mkdir -p "$home/.ssh" "${config%/*}"
chmod "${HOME_MODE:-755}" "$home"
: >"$test_dir/$scenario.calls"
case "${AUTHORIZED_KEY_STATE:-valid}" in
valid) printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" ;;
invalid) printf 'not a public key\n' >"$home/.ssh/authorized_keys" ;;
private) cat "$test_dir/key" >"$home/.ssh/authorized_keys" ;;
esac
if [[ ${LOOSE_SSH_PERMS:-0} == 1 ]]; then
chmod 755 "$home/.ssh"
chmod 644 "$home/.ssh/authorized_keys"
fi
if [[ ${ALREADY_HARDENED:-0} == 1 ]]; then
printf 'PasswordAuthentication no\n' >"$config"
fi
# Keep the privileged production destination fixed in the shipped migration.
# For this isolated test only, rewrite that one assignment in the input fed to
# bash so no scenario can touch the host's /etc.
sed "s|^config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf$|config=$config|" "$migration" |
HOME="$home" CALL_LOG="$test_dir/$scenario.calls" PATH="$stub_bin:$PATH" \
SSHD_ENABLED="${SSHD_ENABLED:-0}" SSHD_ACTIVE="${SSHD_ACTIVE:-0}" \
SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \
SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \
SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \
SSHD_RELOAD_VALID="${SSHD_RELOAD_VALID:-1}" \
SUDO_ALLOWED="${SUDO_ALLOWED:-1}" \
bash -euo pipefail
}
SSHD_ENABLED=0 SSHD_ACTIVE=0 run_migration disabled
[[ ! -e $test_dir/disabled/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration leaves a disabled daemon alone"
! grep -q '^sudo ' "$test_dir/disabled.calls" || fail "disabled SSH does not prompt for privileges"
pass "SSH migration no-ops when sshd is not enabled or active"
ALREADY_HARDENED=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration hardened >/dev/null
[[ ! -s $test_dir/hardened.calls ]] || fail "an already-hardened machine must not touch sshd or prompt"
grep -qxF "PasswordAuthentication no" "$test_dir/hardened/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
fail "the existing hardening config is left alone"
pass "SSH migration no-ops when the hardening config already exists"
AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-key >/dev/null
[[ ! -e $test_dir/no-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration must not disable passwords without an authorized key"
! grep -q '^sudo ' "$test_dir/no-key.calls" || fail "missing SSH key does not prompt for privileges"
AUTHORIZED_KEY_STATE=invalid SSHD_ENABLED=1 run_migration invalid-key >/dev/null
[[ ! -e $test_dir/invalid-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration must not trust a malformed authorized_keys file"
pass "SSH migration requires a usable authorized key before disabling passwords"
# ssh-keygen -lf accepts a whole private-key file, so only a per-line check
# catches the classic `cp id_ed25519 authorized_keys` slip that sshd cannot use.
AUTHORIZED_KEY_STATE=private SSHD_ENABLED=1 run_migration private-key >/dev/null
[[ ! -e $test_dir/private-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration must not treat a private key as an authorized key"
! grep -q '^sudo ' "$test_dir/private-key.calls" || fail "a private-key authorized_keys does not prompt for privileges"
pass "SSH migration rejects an authorized_keys holding a private key"
# StrictModes makes sshd ignore authorized_keys under a group-writable home,
# so the key that validated would be unusable and passwords the only way in.
HOME_MODE=775 SSHD_ENABLED=1 run_migration loose-home >/dev/null
[[ ! -e $test_dir/loose-home/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration must not disable passwords when sshd would ignore the key"
! grep -q '^sudo ' "$test_dir/loose-home.calls" || fail "a group-writable home does not prompt for privileges"
pass "SSH migration leaves a group-writable home directory alone"
LOOSE_SSH_PERMS=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration active >/dev/null
config="$test_dir/active/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
grep -qxF "PasswordAuthentication no" "$config" || fail "SSH migration disables password authentication"
grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH migration disables keyboard-interactive authentication"
[[ $(stat -c '%a' "$test_dir/active/home/.ssh") == "700" ]] ||
fail "SSH migration tightens ~/.ssh so StrictModes accepts the key"
[[ $(stat -c '%a' "$test_dir/active/home/.ssh/authorized_keys") == "600" ]] ||
fail "SSH migration tightens authorized_keys so StrictModes accepts the key"
grep -qxF "sudo sshd -t" "$test_dir/active.calls" || fail "SSH migration validates sshd syntax"
grep -qxF "sudo sshd -T" "$test_dir/active.calls" || fail "SSH migration validates effective sshd settings"
grep -qxF "sudo systemctl reload sshd.service" "$test_dir/active.calls" || fail "SSH migration reloads an active daemon"
pass "SSH migration hardens and reloads an existing key-based SSH setup"
SSHD_ENABLED=1 SSHD_ACTIVE=0 run_migration stopped >/dev/null
[[ -e $test_dir/stopped/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration hardens an enabled but stopped daemon"
! grep -qF 'reload sshd.service' "$test_dir/stopped.calls" || fail "SSH migration must not start or reload a stopped daemon"
pass "SSH migration hardens an enabled daemon without starting it"
# Conditions the migration cannot repair complete with a notice — leaving the
# machine as it was — so they never block the migrations queued behind this one.
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_PASSWORD_AUTH=yes run_migration ineffective >"$test_dir/ineffective.output" 2>&1 ||
fail "an ineffective drop-in must complete without blocking later migrations"
[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration removes an ineffective config"
! grep -qF 'reload sshd.service' "$test_dir/ineffective.calls" || fail "SSH migration must not reload ineffective hardening"
pass "SSH migration backs off when another rule keeps password authentication enabled"
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_SYNTAX_VALID=0 run_migration invalid-config >"$test_dir/invalid-config.output" 2>&1 ||
fail "a rejected config must complete without blocking later migrations"
[[ ! -e $test_dir/invalid-config/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration removes a rejected config"
! grep -qF 'reload sshd.service' "$test_dir/invalid-config.calls" || fail "SSH migration must not reload rejected hardening"
pass "SSH migration backs off when sshd rejects the config"
# The installed config is valid, so a failed reload only delays it until the
# next sshd restart; keep it staged rather than failing or removing it.
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_RELOAD_VALID=0 run_migration reload-fail >"$test_dir/reload-fail.output" 2>&1 ||
fail "a failed reload must complete without blocking later migrations"
[[ -e $test_dir/reload-fail/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "a failed reload keeps the valid hardening config staged"
pass "SSH migration keeps the hardening staged when sshd cannot reload"
# Privileges are the one genuinely retryable failure: stay pending so the
# login notifier prompts for a terminal run.
if SUDO_ALLOWED=0 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration no-sudo >"$test_dir/no-sudo.output" 2>&1; then
fail "SSH migration must stay pending when privileges are unavailable"
fi
[[ ! -e $test_dir/no-sudo/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "no hardening config is left behind without privileges"
pass "SSH migration stays pending until privileges are granted"