From df819a6f9869b080f2d8ba88e3be0b21023d6487 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sun, 30 Aug 2026 12:04:14 -0400 Subject: [PATCH] Close three paths from an unprivileged session to root Apply the Omabot patch on Quattro, verify effective SSH hardening, prevent stored provisioning state from restoring the blanket input-group grant, and stop Omarchy from shipping asdcontrol authorization that belongs to the package. Co-authored-by: David Heinemeier Hansson --- bin/omarchy-provision-owner | 12 ++- bin/omarchy-setup-security-sshd | 45 ++++++++ etc/sudoers.d/omarchy-asdcontrol | 1 - install/hardware/all.sh | 1 - install/hardware/input-group.sh | 11 -- migrations/1787865477.sh | 18 ++++ test/shell.d/input-group-migration-test.sh | 64 +++++++++++ test/shell.d/provisioning-groups-test.sh | 60 +++-------- test/shell.d/setup-security-sshd-test.sh | 105 +++++++++++++++++++ test/shell.d/sudoless-docker-posture-test.sh | 27 +++-- 10 files changed, 274 insertions(+), 70 deletions(-) delete mode 100644 etc/sudoers.d/omarchy-asdcontrol delete mode 100644 install/hardware/input-group.sh create mode 100644 migrations/1787865477.sh create mode 100755 test/shell.d/input-group-migration-test.sh create mode 100755 test/shell.d/setup-security-sshd-test.sh diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index d300d1de..4c21ff49 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -677,11 +677,15 @@ user_groups() { if [[ -f $PROVISIONING_DIR/groups ]]; then while IFS= read -r group; do [[ -n $group ]] || continue - # Never grant docker at first boot, even if an older install recorded it - # (or a factory snapshot predating the opt-in default carries it): the - # docker group is root-equivalent. It is opt-in via - # omarchy-setup-security-sudoless-docker. + # Never replay old privileged group defaults. Docker is always opt-in. + # Input is only retained when the factory image has one of the features + # whose installer deliberately grants access to raw input devices. [[ $group == "docker" ]] && continue + if [[ $group == "input" ]] && + ! pacman -Qq xpadneo-dkms &>/dev/null && + ! pacman -Qq ydotool &>/dev/null; then + continue + fi getent group "$group" >/dev/null || continue [[ ",$groups," == *",$group,"* ]] || groups+=",$group" done <"$PROVISIONING_DIR/groups" diff --git a/bin/omarchy-setup-security-sshd b/bin/omarchy-setup-security-sshd index 155d6000..1f58cd3c 100755 --- a/bin/omarchy-setup-security-sshd +++ b/bin/omarchy-setup-security-sshd @@ -143,6 +143,48 @@ authorize_pasted_key() { authorize_key "$key" || exit 1 } +# Only called after a key is authorized. Disabling password authentication +# before then could lock the owner out of the machine. +disable_password_auth() { + local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf + local effective_config + + if [[ ! -s $AUTHORIZED_KEYS ]]; then + echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2 + return 1 + fi + + echo "Disabling SSH password authentication, now that a key is authorized..." + sudo install -Dm644 /dev/stdin "$config" <<'CONF' +# Written by omarchy-setup-security-sshd once an SSH key was authorized. +# Delete this file and reload sshd to allow password logins again. +PasswordAuthentication no +KbdInteractiveAuthentication no +CONF + + # Validate before reloading: a config sshd rejects would otherwise take the + # service down on its next restart, potentially stranding a remote owner. + if ! sudo sshd -t; then + echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2 + sudo rm -f "$config" + return 1 + fi + + # Syntax alone is insufficient because sshd uses the first value it reads for + # these settings. An earlier administrator rule could leave passwords enabled. + if ! effective_config=$(sudo sshd -T) || + ! grep -qxF "passwordauthentication no" <<<"$effective_config" || + ! grep -qxF "kbdinteractiveauthentication no" <<<"$effective_config"; then + echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2 + sudo rm -f "$config" + return 1 + fi + + # Reload rather than restart so an administrator already connected keeps + # their session. + sudo systemctl reload sshd.service +} + echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m" setup_sshd @@ -161,5 +203,8 @@ else esac fi +disable_password_auth + echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m" +echo "Password logins are off; this machine now accepts authorized keys only." echo "You can now connect with: ssh $USER@$(hostname)" diff --git a/etc/sudoers.d/omarchy-asdcontrol b/etc/sudoers.d/omarchy-asdcontrol deleted file mode 100644 index a8483b62..00000000 --- a/etc/sudoers.d/omarchy-asdcontrol +++ /dev/null @@ -1 +0,0 @@ -%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol diff --git a/install/hardware/all.sh b/install/hardware/all.sh index 9b54d1c0..7b806ce8 100644 --- a/install/hardware/all.sh +++ b/install/hardware/all.sh @@ -4,7 +4,6 @@ run_logged "$OMARCHY_INSTALL/hardware/dell-xps-touchpad-haptics.sh" run_logged "$OMARCHY_INSTALL/hardware/surface.sh" run_logged "$OMARCHY_INSTALL/hardware/network.sh" -run_logged "$OMARCHY_INSTALL/hardware/input-group.sh" run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh" diff --git a/install/hardware/input-group.sh b/install/hardware/input-group.sh deleted file mode 100644 index fac87fcd..00000000 --- a/install/hardware/input-group.sh +++ /dev/null @@ -1,11 +0,0 @@ -# Give this user privileged input access for dictation tools + xbox controllers to work. -# Recorded for provisioning first-boot user creation and factory reset, granted directly -# when the install user already exists (deferred-provisioning installs create the user at -# first boot instead). -provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}" -mkdir -p "$provisioning_dir" -grep -qxF input "$provisioning_dir/groups" 2>/dev/null || echo input >>"$provisioning_dir/groups" - -if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then - usermod -aG input "$OMARCHY_INSTALL_USER" -fi diff --git a/migrations/1787865477.sh b/migrations/1787865477.sh new file mode 100644 index 00000000..4727fb02 --- /dev/null +++ b/migrations/1787865477.sh @@ -0,0 +1,18 @@ +echo "Drop the default input group grant, which allowed unprivileged keylogging" + +# Membership of `input` gives raw read/write access to /dev/input/event*: any +# process running as the user can capture keystrokes and synthesize input. The +# blanket grant is unnecessary: the Xbox-controller and ydotool installers add +# the group themselves when those features are deliberately installed. +# +# Preserve membership where one of those opt-in features is present; removing +# it there would break the feature the user chose to install. +if id -nG "$USER" | grep -qw input; then + if pacman -Qq xpadneo-dkms &>/dev/null || pacman -Qq ydotool &>/dev/null; then + echo "Keeping $USER in the input group: controller or ydotool support is installed." + else + sudo gpasswd -d "$USER" input >/dev/null + echo "Removed $USER from the input group. Log out and back in to apply." + omarchy-state set reboot-required + fi +fi diff --git a/test/shell.d/input-group-migration-test.sh b/test/shell.d/input-group-migration-test.sh new file mode 100755 index 00000000..40bea6cb --- /dev/null +++ b/test/shell.d/input-group-migration-test.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +migration="$ROOT/migrations/1787865477.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +stub_bin="$test_dir/bin" +mkdir -p "$stub_bin" + +cat >"$stub_bin/id" <<'STUB' +#!/bin/bash +printf '%s\n' "${STUB_GROUPS:-wheel}" +STUB +cat >"$stub_bin/pacman" <<'STUB' +#!/bin/bash +[[ $1 == "-Qq" ]] || exit 2 +[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]] +STUB +cat >"$stub_bin/sudo" <<'STUB' +#!/bin/bash +exec "$@" +STUB +cat >"$stub_bin/gpasswd" <<'STUB' +#!/bin/bash +printf '%s\n' "$*" >>"${GPASSWD_CALLS:?}" +STUB +cat >"$stub_bin/omarchy-state" <<'STUB' +#!/bin/bash +printf '%s\n' "$*" >>"${STATE_CALLS:?}" +STUB +chmod +x "$stub_bin"/* + +gpasswd_calls="$test_dir/gpasswd-calls" +state_calls="$test_dir/state-calls" + +run_migration() { + rm -f "$gpasswd_calls" "$state_calls" + USER=tester STUB_GROUPS="$1" STUB_PACKAGES="${2:-}" \ + GPASSWD_CALLS="$gpasswd_calls" STATE_CALLS="$state_calls" \ + PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" +} + +run_migration "wheel input" >/dev/null +grep -qxF -- "-d tester input" "$gpasswd_calls" || fail "migration removes default input membership" +grep -qxF "set reboot-required" "$state_calls" || fail "migration flags the session change for reboot" +pass "migration removes the blanket input grant" + +run_migration "wheel" >/dev/null +[[ ! -e $gpasswd_calls ]] || fail "migration does not remove an already-absent group" +[[ ! -e $state_calls ]] || fail "migration does not flag a reboot when nothing changed" +pass "migration is idempotent after input membership is gone" + +run_migration "wheel input" xpadneo-dkms >/dev/null +[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for controller support" +[[ ! -e $state_calls ]] || fail "preserved controller support does not flag a reboot" + +run_migration "wheel input" ydotool >/dev/null +[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for ydotool" +[[ ! -e $state_calls ]] || fail "preserved ydotool support does not flag a reboot" +pass "migration preserves deliberate input-group opt-ins" diff --git a/test/shell.d/provisioning-groups-test.sh b/test/shell.d/provisioning-groups-test.sh index 5a5fc516..ae47748e 100644 --- a/test/shell.d/provisioning-groups-test.sh +++ b/test/shell.d/provisioning-groups-test.sh @@ -1,11 +1,8 @@ #!/bin/bash # -# The install scripts that grant group memberships must record them in the provisioning -# groups file (for first-boot user creation and factory reset) and only call -# usermod when the install user actually exists. -# -# Docker is deliberately excluded: the docker group is root-equivalent, so it is -# no longer granted at install time (opt in with omarchy-setup-security-sudoless-docker). +# Privileged groups are never granted by the default install. Docker remains an +# explicit opt-in, and raw input-device access is granted only by the optional +# controller and ydotool installers. set -euo pipefail @@ -16,13 +13,7 @@ trap 'rm -rf "$TMPDIR"' EXIT export OMARCHY_PROVISIONING_DIR="$TMPDIR/provisioning" -# Stub getent/usermod: the fake system knows only the user "existing". mkdir -p "$TMPDIR/bin" -cat >"$TMPDIR/bin/getent" <<'STUB' -#!/bin/bash -[[ $1 == passwd && $2 == existing ]] && { echo "existing:x:1000:1000::/home/existing:/bin/bash"; exit 0; } -exit 2 -STUB cat >"$TMPDIR/bin/usermod" <>"$TMPDIR/usermod.calls" @@ -44,48 +35,31 @@ cat >"$TMPDIR/bin/sudo" <>"$TMPDIR/sudo.calls" exec "\$@" STUB -chmod +x "$TMPDIR/bin"/{getent,usermod,groupadd,install,find,sudo} +chmod +x "$TMPDIR/bin"/{usermod,groupadd,install,find,sudo} export PATH="$TMPDIR/bin:$PATH" export OMARCHY_PATH="$ROOT" -# No install user (deferred-provisioning install): groups recorded, usermod not called. +# A deferred-provisioning install records neither privileged group. OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/docker.sh" -OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh" OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" -[[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user" -grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded" -! grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" || - fail "browser-policy group must not be recorded" +[[ ! -f $OMARCHY_PROVISIONING_DIR/groups ]] || + ! grep -Eq '^(docker|input)$' "$OMARCHY_PROVISIONING_DIR/groups" || + fail "default install must not record docker or input groups" [[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user" [[ ! -f $TMPDIR/groupadd.calls ]] || ! grep -F omarchy-browser-policy "$TMPDIR/groupadd.calls" >/dev/null || fail "browser-policy group is not created" grep -F -- '-d -m 0755 -o root -g root /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null || fail "browser-policy directory is created root-owned" -pass "deferred provisioning records groups without calling usermod" +pass "deferred provisioning records no privileged groups" -# The docker group is root-equivalent and must never be granted automatically. -! grep -qxF docker "$OMARCHY_PROVISIONING_DIR/groups" || fail "docker group must not be recorded" -pass "docker group is not recorded at install" - -# Missing user (defensive): no usermod either. -OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/hardware/input-group.sh" -OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/config/browser-policy.sh" -[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called for a missing user" -pass "missing install user defers group grants" - -# Re-running never duplicates entries. -OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh" -[[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once" -OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" -pass "group recording is idempotent" - -# Existing user: usermod applies the recorded groups, and docker is never among them. +# The same remains true when an install user already exists. OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh" -OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh" OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh" -grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user" -! grep -q -- "omarchy-browser-policy" "$TMPDIR/usermod.calls" || - fail "usermod must not grant browser-policy to the install user" -! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user" -pass "existing install user gets input but never docker or browser-policy" +[[ ! -f $TMPDIR/usermod.calls ]] || fail "default install must not grant privileged groups" +pass "existing install user gets neither docker nor input access" + +! grep -q 'hardware/input-group.sh' "$ROOT/install/hardware/all.sh" || + fail "hardware setup must not call the removed input-group grant" +[[ ! -e $ROOT/install/hardware/input-group.sh ]] || fail "blanket input-group grant is removed" +pass "hardware setup has no blanket input-group grant" diff --git a/test/shell.d/setup-security-sshd-test.sh b/test/shell.d/setup-security-sshd-test.sh new file mode 100755 index 00000000..f61d580a --- /dev/null +++ b/test/shell.d/setup-security-sshd-test.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +stub_bin="$test_dir/bin" +mkdir -p "$stub_bin" + +cat >"$stub_bin/omarchy-pkg-add" <<'STUB' +#!/bin/bash +printf 'pkg %s\n' "$*" >>"${CALL_LOG:?}" +STUB +cat >"$stub_bin/omarchy-cmd-missing" <<'STUB' +#!/bin/bash +exit 0 +STUB +cat >"$stub_bin/systemctl" <<'STUB' +#!/bin/bash +printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}" +STUB +cat >"$stub_bin/sshd" <<'STUB' +#!/bin/bash +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 +case $1 in +install) + destination="${TEST_ROOT:?}${4:?}" + /usr/bin/mkdir -p "${destination%/*}" + /usr/bin/install -Dm644 /dev/stdin "$destination" + ;; +rm) + /usr/bin/rm -f "${TEST_ROOT:?}${3:?}" + ;; +*) + exec "$@" + ;; +esac +STUB +chmod +x "$stub_bin"/* + +ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key" +public_key=$(<"$test_dir/key.pub") + +run_setup() { + local scenario="$1" + local home="$test_dir/$scenario/home" + local root="$test_dir/$scenario/root" + + mkdir -p "$home" "$root" + : >"$test_dir/$scenario.calls" + + HOME="$home" TEST_ROOT="$root" CALL_LOG="$test_dir/$scenario.calls" \ + SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \ + SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \ + SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \ + PATH="$stub_bin:$PATH" \ + bash "$ROOT/bin/omarchy-setup-security-sshd" --key="$public_key" +} + +output=$(run_setup success) +config="$test_dir/success/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" +grep -qxF "PasswordAuthentication no" "$config" || fail "SSH setup disables password authentication" +grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH setup disables keyboard-interactive authentication" +grep -qxF "systemctl reload sshd.service" "$test_dir/success.calls" || fail "SSH setup reloads the validated config" +grep -q "Password logins are off" <<<"$output" || fail "SSH setup reports hardening after it succeeds" +pass "SSH setup authorizes a key and disables password logins" + +if SSHD_PASSWORD_AUTH=yes run_setup ineffective >"$test_dir/ineffective.output" 2>&1; then + fail "SSH setup must fail when password authentication remains effective" +fi +[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] || + fail "SSH setup removes an ineffective hardening config" +! grep -qF "systemctl reload sshd.service" "$test_dir/ineffective.calls" || + fail "SSH setup must not reload ineffective hardening" +! grep -q "Password logins are off" "$test_dir/ineffective.output" || + fail "SSH setup must not claim ineffective hardening succeeded" +pass "SSH setup verifies the effective daemon settings" + +if SSHD_SYNTAX_VALID=0 run_setup invalid >"$test_dir/invalid.output" 2>&1; then + fail "SSH setup must fail when sshd rejects its config" +fi +[[ ! -e $test_dir/invalid/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] || + fail "SSH setup removes a rejected hardening config" +! grep -qF "systemctl reload sshd.service" "$test_dir/invalid.calls" || + fail "SSH setup must not reload a rejected config" +! grep -q "Password logins are off" "$test_dir/invalid.output" || + fail "SSH setup must not claim rejected hardening succeeded" +pass "SSH setup fails safely when sshd rejects the config" diff --git a/test/shell.d/sudoless-docker-posture-test.sh b/test/shell.d/sudoless-docker-posture-test.sh index 8b13b31a..a4d6e9fe 100644 --- a/test/shell.d/sudoless-docker-posture-test.sh +++ b/test/shell.d/sudoless-docker-posture-test.sh @@ -1,10 +1,7 @@ #!/bin/bash # -# The docker group is root-equivalent, so no automatic path may grant it. These -# tests guard the paths that are not exercised by a fresh-install run: first-boot -# provisioning replaying a recorded (or factory-snapshot) group list, and the -# Quattro upgrade. Opting in stays a deliberate, warned step -# (omarchy-setup-security-sudoless-docker). +# Docker is root-equivalent, so no automatic path may grant it. Raw input access +# is likewise excluded unless a feature that explicitly needs it is installed. set -euo pipefail @@ -13,11 +10,15 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT -# First-boot provisioning must never grant docker even when it is recorded (an -# older install, or a factory snapshot predating the opt-in default). +# First-boot provisioning must not replay old privileged defaults. mkdir -p "$TMPDIR/bin" printf '#!/bin/bash\nexit 0\n' >"$TMPDIR/bin/getent" # every group "exists" -chmod +x "$TMPDIR/bin/getent" +cat >"$TMPDIR/bin/pacman" <<'STUB' +#!/bin/bash +[[ $1 == "-Qq" ]] || exit 2 +[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]] +STUB +chmod +x "$TMPDIR/bin/getent" "$TMPDIR/bin/pacman" export PATH="$TMPDIR/bin:$PATH" PROVISIONING_DIR="$TMPDIR/prov" @@ -29,9 +30,15 @@ eval "$(sed -n '/^user_groups() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")" groups=$(user_groups) [[ ",$groups," == *",wheel,"* ]] || fail "user_groups always includes wheel" -[[ ",$groups," == *",input,"* ]] || fail "user_groups includes recorded non-docker groups" +[[ ",$groups," != *",input,"* ]] || fail "user_groups must not replay the blanket input grant" [[ ",$groups," == *",docker,"* ]] && fail "user_groups must never grant the docker group" -pass "first-boot user_groups includes recorded groups but never docker" +pass "first-boot user_groups replays neither privileged default" + +groups=$(STUB_PACKAGES=xpadneo-dkms user_groups) +[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed controller support" +groups=$(STUB_PACKAGES=ydotool user_groups) +[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed ydotool support" +pass "first-boot user_groups keeps deliberate input-group opt-ins" # The Quattro upgrade must not re-add the user to docker. if rg -q 'usermod -aG docker' "$ROOT/bin/omarchy-upgrade-to-quattro"; then