Merge pull request #9267 from omacom/fix/close-password-only-sshd

Disable sshd entirely when no usable key is authorized
This commit is contained in:
Ryan Hughes
2026-08-30 18:46:35 -04:00
committed by GitHub
2 changed files with 65 additions and 11 deletions
+21 -4
View File
@@ -1,4 +1,4 @@
echo "Disable SSH password authentication on existing key-based setups" echo "Disable SSH password authentication, or sshd itself when no key is authorized"
config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
authorized_keys="$HOME/.ssh/authorized_keys" authorized_keys="$HOME/.ssh/authorized_keys"
@@ -53,9 +53,26 @@ has_usable_key() {
return 1 return 1
} }
if [[ ! -f $authorized_keys || -L $authorized_keys || ! -s $authorized_keys || ! -r $authorized_keys ]] || # A file that exists but cannot be read leaves the key question unanswered; do
! has_usable_key; then # not treat it as proof the machine is password-only. [[ -f ]] and the read
skip "$authorized_keys has no usable public key." # both follow symlinks on purpose: a dotfiles-managed authorized_keys link with
# a working key must not count as keyless.
if [[ -f $authorized_keys && ! -r $authorized_keys ]]; then
skip "Could not read $authorized_keys to check for a usable key."
fi
# The old setup command enabled sshd before importing a key, so an aborted run
# left a password-only server exposed. Without a usable key there is nothing to
# harden: close the hole Omarchy opened by disabling the server. Omarchy is a
# desktop distro, so the console remains; re-enabling password SSH afterwards
# is an intentional, informed choice the warning explains how to make.
if [[ ! -f $authorized_keys ]] || ! has_usable_key; then
if ! as_root systemctl disable --now sshd.service; then
echo "Administrator privileges are required to close the password-only SSH server. Run omarchy-migrate again from a terminal." >&2
exit 1
fi
echo "No usable SSH key is authorized, so sshd only accepted password logins. The SSH server has been disabled: run omarchy-setup-security-sshd to set it up with key-based authentication, or re-enable sshd to accept password logins anyway."
exit 0
fi fi
# Under StrictModes, sshd's default, a group- or world-writable home directory, # Under StrictModes, sshd's default, a group- or world-writable home directory,
+44 -7
View File
@@ -18,6 +18,7 @@ case "$1 $2" in
"is-enabled --quiet") [[ ${SSHD_ENABLED:-0} == 1 ]] ;; "is-enabled --quiet") [[ ${SSHD_ENABLED:-0} == 1 ]] ;;
"is-active --quiet") [[ ${SSHD_ACTIVE:-0} == 1 ]] ;; "is-active --quiet") [[ ${SSHD_ACTIVE:-0} == 1 ]] ;;
"reload sshd.service") [[ ${SSHD_RELOAD_VALID:-1} == 1 ]] ;; "reload sshd.service") [[ ${SSHD_RELOAD_VALID:-1} == 1 ]] ;;
"disable --now") ;;
*) exit 2 ;; *) exit 2 ;;
esac esac
STUB STUB
@@ -62,6 +63,14 @@ run_migration() {
valid) printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" ;; valid) printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" ;;
invalid) printf 'not a public key\n' >"$home/.ssh/authorized_keys" ;; invalid) printf 'not a public key\n' >"$home/.ssh/authorized_keys" ;;
private) cat "$test_dir/key" >"$home/.ssh/authorized_keys" ;; private) cat "$test_dir/key" >"$home/.ssh/authorized_keys" ;;
symlink)
printf '%s\n' "$public_key" >"$home/.ssh/imported_key"
ln -s imported_key "$home/.ssh/authorized_keys"
;;
unreadable)
printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys"
chmod 000 "$home/.ssh/authorized_keys"
;;
esac esac
if [[ ${LOOSE_SSH_PERMS:-0} == 1 ]]; then if [[ ${LOOSE_SSH_PERMS:-0} == 1 ]]; then
chmod 755 "$home/.ssh" chmod 755 "$home/.ssh"
@@ -85,6 +94,10 @@ run_migration() {
bash -euo pipefail bash -euo pipefail
} }
sshd_disabled() {
grep -qxF "sudo systemctl disable --now sshd.service" "$test_dir/$1.calls"
}
SSHD_ENABLED=0 SSHD_ACTIVE=0 run_migration disabled SSHD_ENABLED=0 SSHD_ACTIVE=0 run_migration disabled
[[ ! -e $test_dir/disabled/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] || [[ ! -e $test_dir/disabled/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration leaves a disabled daemon alone" fail "SSH migration leaves a disabled daemon alone"
@@ -97,23 +110,42 @@ grep -qxF "PasswordAuthentication no" "$test_dir/hardened/root/etc/ssh/sshd_conf
fail "the existing hardening config is left alone" fail "the existing hardening config is left alone"
pass "SSH migration no-ops when the hardening config already exists" pass "SSH migration no-ops when the hardening config already exists"
# Without a usable key, sshd only accepts password logins — the hole the old
# setup command could leave open. The migration closes it by disabling sshd.
AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-key >/dev/null 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 ]] || [[ ! -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" fail "SSH migration must not write the hardening config without an authorized key"
! grep -q '^sudo ' "$test_dir/no-key.calls" || fail "missing SSH key does not prompt for privileges" sshd_disabled no-key || fail "SSH migration disables a password-only sshd"
pass "SSH migration disables sshd when no key is authorized"
AUTHORIZED_KEY_STATE=invalid SSHD_ENABLED=1 run_migration invalid-key >/dev/null 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 ]] || sshd_disabled invalid-key || fail "a malformed authorized_keys leaves sshd password-only"
fail "SSH migration must not trust a malformed authorized_keys file" pass "SSH migration disables sshd when authorized_keys holds no valid key"
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 # 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. # 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 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 ]] || [[ ! -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" 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" sshd_disabled private-key || fail "a private-key authorized_keys leaves sshd password-only"
pass "SSH migration rejects an authorized_keys holding a private key" pass "SSH migration disables sshd when authorized_keys holds a private key"
# A dotfiles-managed symlink with a working key is a key-based setup, not a
# keyless one; it must be hardened, never disabled.
AUTHORIZED_KEY_STATE=symlink SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration symlink-key >/dev/null
grep -qxF "PasswordAuthentication no" "$test_dir/symlink-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
fail "SSH migration hardens a symlinked authorized_keys with a valid key"
! sshd_disabled symlink-key || fail "SSH migration must not disable sshd when the symlinked key is usable"
pass "SSH migration follows an authorized_keys symlink to its key"
# An unreadable file answers neither "keyless" nor "key-based": touch nothing.
if (( EUID != 0 )); then
AUTHORIZED_KEY_STATE=unreadable SSHD_ENABLED=1 run_migration unreadable >/dev/null
[[ ! -e $test_dir/unreadable/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "SSH migration must not harden against an unverifiable authorized_keys"
! grep -q '^sudo ' "$test_dir/unreadable.calls" || fail "an unreadable authorized_keys does not prompt or disable"
pass "SSH migration leaves an unreadable authorized_keys alone"
fi
# StrictModes makes sshd ignore authorized_keys under a group-writable home, # 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. # so the key that validated would be unusable and passwords the only way in.
@@ -174,3 +206,8 @@ fi
[[ ! -e $test_dir/no-sudo/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] || [[ ! -e $test_dir/no-sudo/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
fail "no hardening config is left behind without privileges" fail "no hardening config is left behind without privileges"
pass "SSH migration stays pending until privileges are granted" pass "SSH migration stays pending until privileges are granted"
if SUDO_ALLOWED=0 AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-sudo-keyless >"$test_dir/no-sudo-keyless.output" 2>&1; then
fail "SSH migration must stay pending when it cannot disable a password-only sshd"
fi
pass "SSH migration stays pending when disabling sshd needs privileges"