From ca4f596a14a12b26fa17ba06d49a7f166e5f6767 Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Sun, 30 Aug 2026 15:17:33 -0600 Subject: [PATCH] Harden existing key-based SSH setups --- migrations/1788124236.sh | 77 +++++++++++ test/shell.d/sshd-hardening-migration-test.sh | 121 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 migrations/1788124236.sh create mode 100755 test/shell.d/sshd-hardening-migration-test.sh diff --git a/migrations/1788124236.sh b/migrations/1788124236.sh new file mode 100644 index 00000000..824fc58f --- /dev/null +++ b/migrations/1788124236.sh @@ -0,0 +1,77 @@ +echo "Disable SSH password authentication on existing key-based setups" + +config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf +authorized_keys="$HOME/.ssh/authorized_keys" + +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} + +# The fixed setup command writes this file itself. Its presence is also the +# machine-wide completion state, so migrations run by another account no-op. +if [[ -e $config || -L $config ]]; then + exit 0 +fi + +# Earlier versions enabled sshd before importing the key, but did not leave a +# marker saying that Omarchy configured it. Limit the repair to a daemon that is +# enabled or currently exposed and a user who already has a usable authorized +# key. A machine that never set SSH up exits without prompting for privileges. +if ! systemctl is-enabled --quiet sshd.service 2>/dev/null && + ! systemctl is-active --quiet sshd.service 2>/dev/null; then + exit 0 +fi + +if [[ ! -f $authorized_keys || -L $authorized_keys || ! -s $authorized_keys || ! -r $authorized_keys ]] || + ! ssh-keygen -lf "$authorized_keys" >/dev/null 2>&1; then + echo "Leaving SSH password authentication unchanged because $authorized_keys has no usable public key." + exit 0 +fi + +echo "Disabling SSH password authentication on the existing key-based SSH setup..." +if ! as_root install -Dm644 /dev/stdin "$config" <<'CONF' +# Written by Omarchy once an SSH key was already authorized. +# Delete this file and reload sshd to allow password logins again. +PasswordAuthentication no +KbdInteractiveAuthentication no +CONF +then + echo "Administrator privileges are required to harden the existing SSH setup. Run omarchy-migrate again from a terminal." >&2 + exit 1 +fi + +# Syntax alone is insufficient because sshd uses the first value it reads. If +# another administrator rule wins, remove our ineffective file and keep the +# migration pending rather than claiming the machine is protected. +if ! as_root sshd -t; then + as_root rm -f -- "$config" || true + echo "sshd rejected the hardening config. Fix the SSH configuration and run omarchy-migrate again." >&2 + exit 1 +fi + +effective_config=$(as_root sshd -T) || { + as_root rm -f -- "$config" || true + echo "Could not inspect sshd's effective configuration. Run omarchy-migrate again after fixing SSH." >&2 + exit 1 +} + +if ! grep -qixF "passwordauthentication no" <<<"$effective_config" || + ! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then + as_root rm -f -- "$config" || true + echo "Another SSH rule keeps password authentication enabled. Fix its ordering and run omarchy-migrate again." >&2 + exit 1 +fi + +# An enabled but deliberately stopped daemon picks the file up on its next +# start. Reload only a daemon that is currently serving connections so existing +# sessions survive while new ones get the hardened policy. +if systemctl is-active --quiet sshd.service 2>/dev/null; then + if ! as_root systemctl reload sshd.service; then + echo "The hardening config is valid but sshd could not reload it. Run omarchy-migrate again after fixing the service." >&2 + exit 1 + fi +fi diff --git a/test/shell.d/sshd-hardening-migration-test.sh b/test/shell.d/sshd-hardening-migration-test.sh new file mode 100755 index 00000000..c5659eb6 --- /dev/null +++ b/test/shell.d/sshd-hardening-migration-test.sh @@ -0,0 +1,121 @@ +#!/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:?}" +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%/*}" + : >"$test_dir/$scenario.calls" + if [[ ${AUTHORIZED_KEY_STATE:-valid} == "valid" ]]; then + printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" + elif [[ $AUTHORIZED_KEY_STATE == "invalid" ]]; then + printf 'not a public key\n' >"$home/.ssh/authorized_keys" + 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}" \ + 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" + +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" + +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" +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" + +if SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_PASSWORD_AUTH=yes run_migration ineffective >"$test_dir/ineffective.output" 2>&1; then + fail "SSH migration must fail when password authentication remains effective" +fi +[[ ! -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 stays pending when another rule keeps password authentication enabled" + +if SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_SYNTAX_VALID=0 run_migration invalid-config >"$test_dir/invalid-config.output" 2>&1; then + fail "SSH migration must fail when sshd rejects its config" +fi +[[ ! -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 fails safely when sshd rejects the config"