diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index f4bff333..a4f2d996 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -747,12 +747,6 @@ create_user() { echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel chmod 440 /etc/sudoers.d/00-omarchy-wheel - if omarchy-pkg-present usbguard; then - source "$OMARCHY_PATH/install/helpers/usb-authorization.sh" - usb_authorization_add_user "$username" - systemctl try-restart usbguard.service - fi - source "$OMARCHY_PATH/install/helpers/browser-policy.sh" for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do [[ -d $dir || -L $dir ]] || continue @@ -1040,6 +1034,12 @@ run_provisioning() { limine-update fi + if omarchy-pkg-present usbguard; then + log_step "enrolling the owner's USB devices" + source "$OMARCHY_PATH/install/helpers/usb-authorization.sh" + usb_authorization_provision_owner "$username" + fi + log_step "cleaning up provisioning state" cleanup_oem_state log_step "first-boot setup complete" diff --git a/install/config/usb-authorization.sh b/install/config/usb-authorization.sh index 958b82ae..6b0747e8 100644 --- a/install/config/usb-authorization.sh +++ b/install/config/usb-authorization.sh @@ -1,5 +1,12 @@ echo "Configure USB device authorization" +# A deferred install has only the builder's peripherals attached. The owner +# needs working input before there is an account or a session for approvals. +if [[ -z ${OMARCHY_INSTALL_USER:-} ]]; then + systemctl disable usbguard.service + return 0 +fi + rules_file="${OMARCHY_USB_AUTHORIZATION_RULES_FILE:-/etc/usbguard/rules.conf}" daemon_config="${OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG:-/etc/usbguard/usbguard-daemon.conf}" @@ -7,21 +14,10 @@ source "$OMARCHY_INSTALL/helpers/usb-authorization.sh" usb_authorization_require_secure_settings "$daemon_config" if [[ ! -s $rules_file ]]; then - policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX") - chmod 600 "$policy" - - if usb_authorization_generate_policy "$policy"; then - install -Dm600 -o root -g root "$policy" "$rules_file" - rm -f "$policy" - else - rm -f "$policy" - return 1 - fi + usb_authorization_install_policy "$rules_file" fi -if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then - usb_authorization_add_user "$OMARCHY_INSTALL_USER" -fi +usb_authorization_add_user "$OMARCHY_INSTALL_USER" # Installs are followed by a reboot, so enable the daemon without starting it # in the live ISO's chroot. diff --git a/install/helpers/usb-authorization.sh b/install/helpers/usb-authorization.sh index a99fbfb9..70e36628 100644 --- a/install/helpers/usb-authorization.sh +++ b/install/helpers/usb-authorization.sh @@ -49,11 +49,11 @@ usb_authorization_require_secure_setting() { usb_authorization_require_secure_settings() { local config="$1" - usb_authorization_require_secure_setting "$config" ImplicitPolicyTarget block - usb_authorization_require_secure_setting "$config" PresentDevicePolicy apply-policy - usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy - usb_authorization_require_secure_setting "$config" AuthorizedDefault none - usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false + usb_authorization_require_secure_setting "$config" ImplicitPolicyTarget block || return 1 + usb_authorization_require_secure_setting "$config" PresentDevicePolicy apply-policy || return 1 + usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy || return 1 + usb_authorization_require_secure_setting "$config" AuthorizedDefault none || return 1 + usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false || return 1 } usb_authorization_generate_policy() { @@ -85,6 +85,33 @@ usb_authorization_add_user() { --exceptions=listen } +usb_authorization_install_policy() { + local rules="$1" policy status=0 + + policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX") || return 1 + if usb_authorization_generate_policy "$policy"; then + install -Dm600 -o root -g root "$policy" "$rules" || status=$? + else + status=1 + fi + rm -f "$policy" + return "$status" +} + +usb_authorization_provision_owner() { + local user="$1" + local rules="${2:-/etc/usbguard/rules.conf}" + local config="${3:-/etc/usbguard/usbguard-daemon.conf}" + + usb_authorization_require_secure_settings "$config" || return 1 + # Enroll the owner's hardware, replacing any policy inherited from the + # builder. Do this only after the interactive provisioning steps finish. + usb_authorization_install_policy "$rules" || return 1 + usb_authorization_add_user "$user" || return 1 + systemctl enable usbguard.service || return 1 + systemctl restart usbguard.service +} + usb_authorization_trust_present_devices() { local line id local count=0 diff --git a/manual/48-security.md b/manual/48-security.md index ecce35a2..01e805b6 100644 --- a/manual/48-security.md +++ b/manual/48-security.md @@ -13,6 +13,8 @@ Omarchy takes security extremely seriously. This is meant to be an operating sys USB device authorization is enabled by default. Omarchy uses USBGuard to trust the devices connected during installation and block USB devices it has not seen before. When a device is blocked, click the persistent notification and choose whether to allow it once, always allow it, or keep it blocked. +On a preinstalled machine that asks you to create your account at first boot, USB protection starts at the end of that setup. It trusts the devices connected then, so your keyboard works throughout account setup even if it differs from the installer's keyboard. + You can turn the protection off from _Remove > Security > USB Device Authorization_. _Setup > Security > USB Device Authorization_ turns it back on with the existing trusted-device policy. Only approve hardware you recognize. A malicious USB device can forge the manufacturer, model, serial number, and device class shown during review. Permanent approval is a convenience for accessories you control, not proof of the device's identity. diff --git a/test/shell.d/usb-authorization-test.sh b/test/shell.d/usb-authorization-test.sh index 6017e18d..c04d3acc 100755 --- a/test/shell.d/usb-authorization-test.sh +++ b/test/shell.d/usb-authorization-test.sh @@ -62,7 +62,7 @@ generate-policy) echo 'usbguard ' >>"$CALLS" [[ ${GENERATE_FAIL:-0} == 0 ]] || exit 1 if [[ ${GENERATE_EMPTY:-0} == 0 ]]; then - echo 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' + printf '%s\n' "${GENERATED_RULE:-allow id 1d6b:0002 name \"Linux Foundation root hub\" hash \"root\"}" fi ;; list-devices) @@ -357,10 +357,8 @@ grep -Fq 'config/usb-authorization.sh' "$ROOT/install/config/all.sh" || fail "fresh installs configure the USB authorization policy" grep -Fq 'omarchy-usb-authorization.service' "$ROOT/install/user/first-run/enable-user-units.sh" || fail "fresh installs enable the USB approval watcher" -grep -Fq 'usb_authorization_add_user "$username"' "$ROOT/bin/omarchy-provision-owner" || - fail "deferred provisioning grants the new owner approval access" -grep -Fq 'systemctl try-restart usbguard.service' "$ROOT/bin/omarchy-provision-owner" || - fail "deferred provisioning reloads the new owner ACL" +grep -Fq 'usb_authorization_provision_owner "$username"' "$ROOT/bin/omarchy-provision-owner" || + fail "deferred provisioning enrolls the owner before finishing" grep -Fqx 'omarchy-setup-security-usb-authorization --yes' "$ROOT/migrations/1789433473.sh" || fail "existing installs enable USB authorization during migration" @@ -391,6 +389,39 @@ fi ! grep -Fq 'systemctl ' "$calls" || fail "failed enumeration must not enable USBGuard" pass "fresh installation distinguishes no USB hardware from enumeration failure" +: >"$rules" +: >"$calls" +OMARCHY_INSTALL_USER="" bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-deferred-output" +[[ ! -s $rules ]] || fail "deferred installation must not enroll the builder's hardware" +! grep -q '^usbguard' "$calls" || fail "deferred installation must not enumerate or grant owner ACLs" +grep -Fqx 'systemctl ' "$calls" || fail "deferred installation leaves enforcement disabled" +! grep -Fq 'systemctl ' "$calls" || fail "deferred installation must not enable enforcement" + +source "$ROOT/install/helpers/usb-authorization.sh" +echo 'allow id 1234:0001 name "Builder keyboard" hash "builder"' >"$rules" +owner_rule='allow id 1234:0002 name "Owner keyboard" hash "owner"' +: >"$calls" +GENERATED_RULE="$owner_rule" usb_authorization_provision_owner owner "$rules" "$daemon_config" +[[ $(<"$rules") == "$owner_rule" ]] || fail "owner enrollment must replace inherited builder trust" +grep -Fqx 'usbguard <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" || + fail "owner provisioning grants approval access" +[[ $(tail -2 "$calls") == $'systemctl \nsystemctl ' ]] || + fail "enforcement starts only after owner policy and ACL setup" + +: >"$calls" +if GENERATE_FAIL=1 usb_authorization_provision_owner owner "$rules" "$daemon_config" >"$scratch/owner-failed" 2>&1; then + fail "owner enrollment must reject enumeration failure" +fi +[[ $(<"$rules") == "$owner_rule" ]] || fail "failed enrollment preserves the old policy" +! grep -q '^systemctl' "$calls" || fail "failed owner enrollment must not enable enforcement" +: >"$calls" +sed 's/ImplicitPolicyTarget=block/ImplicitPolicyTarget=allow/' "$daemon_config" >"$scratch/insecure-daemon.conf" +if usb_authorization_provision_owner owner "$rules" "$scratch/insecure-daemon.conf" >"$scratch/owner-insecure" 2>&1; then + fail "owner enrollment rejects insecure settings even when called in a conditional" +fi +[[ ! -s $calls ]] || fail "invalid settings must prevent all enrollment changes" +pass "deferred installs enroll owner hardware before enabling enforcement" + malicious_rule='block id 07a6:8513 name "$(touch '"$scratch"'/injected)" hash "attacker" with-interface 02:06:00' export BLOCKED_DEVICE_RULE="$malicious_rule" notification_count=$(grep -c '^notification' "$calls" || true)