diff --git a/test/acceptance.d/security-test.sh b/test/acceptance.d/security-test.sh new file mode 100755 index 00000000..0755c722 --- /dev/null +++ b/test/acceptance.d/security-test.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# +# Verifies the security posture of an installed system: the unprivileged +# session-to-root paths closed for 4.0.2 (blanket input-group grant, shipped +# asdcontrol sudoers authorization) and the SSH hardening flow. +# +# The sshd section reconfigures the machine (enables sshd, opens the firewall, +# disables password logins), so it demands explicit opt-in: it only runs when +# OMARCHY_ACCEPTANCE_SUDO_PASSWORD is set, which omarchy-iso-test does for its +# throwaway VMs. A cached sudo timestamp alone never triggers it, so running +# the suite on a machine you care about cannot reconfigure sshd by accident. + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +# Membership of `input` gives raw access to /dev/input/event*: any process +# running as the user could log keystrokes. Only the opt-in controller and +# ydotool features may grant it. +verify_input_group() { + if id -nG | grep -qw input; then + if pacman -Q xpadneo-dkms &>/dev/null || pacman -Q ydotool &>/dev/null; then + pass "input group membership is backed by an opt-in feature" + else + fail "user is not in the input group" "no controller or ydotool support installed to justify it" + fi + else + pass "user is not in the input group" + fi +} + +sudo_available() { + if sudo -n true 2>/dev/null; then + return 0 + fi + + if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]]; then + printf '%s\n' "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null + return $? + fi + + return 1 +} + +verify_asdcontrol_sudoers() { + # Omarchy used to ship a passwordless sudoers grant for asdcontrol; that + # authorization now belongs to the package alone. + if sudo -n test -e /etc/sudoers.d/omarchy-asdcontrol; then + fail "no omarchy asdcontrol sudoers grant is shipped" "/etc/sudoers.d/omarchy-asdcontrol exists" + fi + pass "no omarchy asdcontrol sudoers grant is shipped" +} + +verify_sshd_hardening() { + local key_file=/tmp/omarchy-acceptance-sshd-key + local effective_config + + rm -f "$key_file" "$key_file.pub" + ssh-keygen -t ed25519 -N "" -q -C "omarchy-acceptance" -f "$key_file" + + if ! omarchy-setup-security-sshd --key="$(cat "$key_file.pub")" >"$ARTIFACTS/setup-security-sshd.log" 2>&1; then + fail "omarchy-setup-security-sshd completes unattended" "$(tail -5 "$ARTIFACTS/setup-security-sshd.log")" + fi + pass "omarchy-setup-security-sshd completes unattended" + + systemctl is-active sshd.service >/dev/null || fail "sshd is running after setup" + pass "sshd is running after setup" + + grep -qxF "$(cat "$key_file.pub")" "$HOME/.ssh/authorized_keys" || fail "the key is authorized" + pass "the key is authorized" + + # The command verifies its own hardening before keeping it, but assert the + # effective config independently: sshd honors the first value it reads, and + # regressions here reopen password logins. Keywords match case-insensitively + # because OpenSSH 9.x dumps them lowercase and 10.x in CamelCase. + effective_config=$(sudo -n sshd -T) || fail "sshd reports its effective config" + grep -qixF "passwordauthentication no" <<<"$effective_config" || fail "password authentication is off" + pass "password authentication is off" + grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config" || fail "keyboard-interactive authentication is off" + pass "keyboard-interactive authentication is off" + + if omarchy-cmd-present ufw; then + sudo -n ufw status | grep -qE '^22/tcp\s+LIMIT' || fail "the SSH port is rate limited in the firewall" + pass "the SSH port is rate limited in the firewall" + fi + + # Leave the machine as found where cheap: the throwaway key stays useless + # once removed, while the hardening itself is the state under test. + sed -i "\#$(cat "$key_file.pub" | cut -d' ' -f2)#d" "$HOME/.ssh/authorized_keys" + rm -f "$key_file" "$key_file.pub" +} + +verify_input_group + +if sudo_available; then + verify_asdcontrol_sudoers +else + pass "asdcontrol sudoers check skipped: sudo needs a password" +fi + +if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]] && sudo_available; then + verify_sshd_hardening +else + pass "sshd hardening exercise skipped: set OMARCHY_ACCEPTANCE_SUDO_PASSWORD to run it" +fi