Require approval for new USB devices by default
Co-authored-by: Outfoxxed <outfoxxed@outfoxxed.me>
This commit is contained in:
1 parent
86a2e5830e
commit
44f68f4329
19 files changed
+1382
-2
No files matched your search
@@ -747,6 +747,12 @@ create_user() {
|
|||||||
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
||||||
chmod 440 /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"
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||||
[[ -d $dir || -L $dir ]] || continue
|
[[ -d $dir || -L $dir ]] || continue
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Stop asking before new USB devices can access the system
|
||||||
|
# omarchy:args=[--boot-only] [--yes]
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
assume_yes=0
|
||||||
|
boot_only=0
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--yes) assume_yes=1 ;;
|
||||||
|
--boot-only) boot_only=1 ;;
|
||||||
|
-h | --help)
|
||||||
|
echo "Usage: omarchy-remove-security-usb-authorization [--boot-only] [--yes]"
|
||||||
|
echo
|
||||||
|
echo "Disables Omarchy's USB approval prompts and restores the kernel's"
|
||||||
|
echo "normal default-allow behavior."
|
||||||
|
echo "Use --boot-only to keep USBGuard enabled after the system starts."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "omarchy-remove-security-usb-authorization: unknown option '$1'. Try --help." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( boot_only )); then
|
||||||
|
if (( ! assume_yes )); then
|
||||||
|
gum style --foreground 212 --bold "Remove USB authorization from boot"
|
||||||
|
echo
|
||||||
|
echo "USBGuard will continue blocking unknown devices after its service starts."
|
||||||
|
echo "Devices can reach kernel drivers earlier during boot."
|
||||||
|
echo
|
||||||
|
gum confirm "Disable USB authorization from kernel boot?" || exit 130
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo omarchy-usb-authorization-boot disable
|
||||||
|
echo
|
||||||
|
echo "Boot-time USB authorization is disabled and takes effect after reboot."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
state_dir="$HOME/.local/state/omarchy/usb-authorization"
|
||||||
|
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
|
||||||
|
|
||||||
|
if (( ! assume_yes )); then
|
||||||
|
gum style --foreground 212 --bold "Remove USB device authorization"
|
||||||
|
echo
|
||||||
|
echo "New USB devices will be allowed to bind to kernel drivers without approval."
|
||||||
|
echo
|
||||||
|
gum confirm "Disable USB device authorization?" || exit 130
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuild a permissive boot image before stopping USBGuard. If rebuilding
|
||||||
|
# fails, the command stops here and leaves the complete policy enabled.
|
||||||
|
sudo omarchy-usb-authorization-boot disable
|
||||||
|
|
||||||
|
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
|
||||||
|
rm -f "$unit"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
if omarchy-pkg-present usbguard; then
|
||||||
|
sudo usbguard remove-user "$USER" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if omarchy-pkg-present usbguard; then
|
||||||
|
sudo systemctl disable --now usbguard.service 2>/dev/null || true
|
||||||
|
sudo omarchy-usb-authorization-restore-default
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d $state_dir/requests ]]; then
|
||||||
|
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
|
||||||
|
rmdir "$state_dir/requests" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$state_dir/package-installed-by-omarchy" \
|
||||||
|
"$state_dir/policy-generated-by-omarchy" \
|
||||||
|
"$state_dir/service-enabled-by-omarchy"
|
||||||
|
rmdir "$state_dir" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "USB device authorization is disabled."
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Ask before new USB devices can access the system
|
||||||
|
# omarchy:args=[--boot] [--yes]
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
assume_yes=0
|
||||||
|
from_boot=0
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--yes) assume_yes=1 ;;
|
||||||
|
--boot) from_boot=1 ;;
|
||||||
|
-h | --help)
|
||||||
|
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes]"
|
||||||
|
echo
|
||||||
|
echo "Trusts USB devices that are connected now, then blocks new devices"
|
||||||
|
echo "until you approve them from an Omarchy notification."
|
||||||
|
echo
|
||||||
|
echo "--boot also blocks USB devices from the start of kernel boot. Connected"
|
||||||
|
echo "devices are added to USBGuard's policy, but remain unavailable before"
|
||||||
|
echo "USBGuard starts, including at an encrypted-disk password prompt."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
state_dir="$HOME/.local/state/omarchy/usb-authorization"
|
||||||
|
unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service"
|
||||||
|
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
|
||||||
|
rules_file=/etc/usbguard/rules.conf
|
||||||
|
daemon_config=/etc/usbguard/usbguard-daemon.conf
|
||||||
|
|
||||||
|
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
|
||||||
|
|
||||||
|
if (( ! assume_yes )); then
|
||||||
|
if (( from_boot )); then
|
||||||
|
gum style --foreground 212 --bold "USB authorization from boot"
|
||||||
|
else
|
||||||
|
gum style --foreground 212 --bold "USB device authorization"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "Devices connected now will be trusted. Disconnect anything you do not trust."
|
||||||
|
echo "New USB devices will remain blocked until you approve them."
|
||||||
|
if (( from_boot )); then
|
||||||
|
echo
|
||||||
|
echo "Boot-time protection blocks all USB devices until USBGuard starts."
|
||||||
|
echo "A USB keyboard will not work at an encrypted-disk password prompt, even"
|
||||||
|
echo "when it is connected now. Continue only if unlock and recovery do not"
|
||||||
|
echo "depend on USB input."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
if (( from_boot )); then
|
||||||
|
gum confirm "Enable USB authorization from kernel boot?" || exit 130
|
||||||
|
else
|
||||||
|
gum confirm "Enable USB device authorization?" || exit 130
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
omarchy-pkg-add usbguard
|
||||||
|
|
||||||
|
usb_authorization_require_secure_settings "$daemon_config"
|
||||||
|
|
||||||
|
mkdir -p "$state_dir" "${unit%/*}"
|
||||||
|
chmod 700 "$state_dir"
|
||||||
|
|
||||||
|
if ! usb_authorization_rules_present "$rules_file"; then
|
||||||
|
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX")
|
||||||
|
trap 'rm -f "$policy"' EXIT
|
||||||
|
chmod 600 "$policy"
|
||||||
|
usb_authorization_generate_policy "$policy"
|
||||||
|
sudo install -Dm600 -o root -g root "$policy" "$rules_file"
|
||||||
|
touch "$state_dir/policy-generated-by-omarchy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This user can inspect USBGuard events and approve the device named by an
|
||||||
|
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
|
||||||
|
sudo usbguard add-user "$USER" \
|
||||||
|
--devices=list,listen,modify \
|
||||||
|
--policy=list \
|
||||||
|
--exceptions=listen
|
||||||
|
|
||||||
|
sudo systemctl enable usbguard.service
|
||||||
|
# add-user ACL changes are loaded only when the daemon starts, so restart an
|
||||||
|
# existing service as well as starting a fresh one.
|
||||||
|
sudo systemctl restart usbguard.service
|
||||||
|
|
||||||
|
ln -sfn "$unit_source" "$unit"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now omarchy-usb-authorization.service
|
||||||
|
|
||||||
|
if (( from_boot )); then
|
||||||
|
# Take the trust snapshot only after USBGuard is enforcing the policy. Every
|
||||||
|
# ID is resolved by the daemon, and no device-controlled descriptor text is
|
||||||
|
# passed to a privileged command.
|
||||||
|
usb_authorization_trust_present_devices
|
||||||
|
sudo omarchy-usb-authorization-boot enable
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "USB device authorization is enabled."
|
||||||
|
echo "Devices connected during setup remain trusted; new devices require approval."
|
||||||
|
if (( from_boot )); then
|
||||||
|
echo "Boot-time authorization is enabled and takes effect after reboot."
|
||||||
|
fi
|
||||||
Executable
+432
@@ -0,0 +1,432 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Manage USB authorization from the start of kernel boot
|
||||||
|
# omarchy:args=<enable|disable>
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
drop_in=/etc/limine-entry-tool.d/usb-authorization.conf
|
||||||
|
setting='KERNEL_CMDLINE[default]+=" usbcore.authorized_default=0"'
|
||||||
|
limine_conf=/boot/limine.conf
|
||||||
|
limine_defaults=/etc/default/limine
|
||||||
|
boot_path=/boot
|
||||||
|
kernel_parameter=usbcore.authorized_default=0
|
||||||
|
snapshot_begin='# Omarchy USB authorization begin'
|
||||||
|
snapshot_setting='SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0'
|
||||||
|
snapshot_end='# Omarchy USB authorization end'
|
||||||
|
|
||||||
|
usb_authorization_cmdline_has_parameter() {
|
||||||
|
local cmdline="$1" parameter="$2" word
|
||||||
|
|
||||||
|
for word in $cmdline; do
|
||||||
|
[[ $word == "$parameter" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_secure_boot_enabled() {
|
||||||
|
local value variable
|
||||||
|
|
||||||
|
for variable in /sys/firmware/efi/efivars/SecureBoot-*; do
|
||||||
|
[[ -r $variable ]] || continue
|
||||||
|
value=$(od -An -t u1 -j 4 -N 1 "$variable")
|
||||||
|
value=${value//[[:space:]]/}
|
||||||
|
[[ $value == "1" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_verify_boot_entries() {
|
||||||
|
local expected="$1" config="$2" esp="$3"
|
||||||
|
local machine_id="${4:-$(</etc/machine-id)}"
|
||||||
|
local secure_boot="${5:-auto}"
|
||||||
|
local actual_hash embedded_cmdline expected_hash image mode path paths section
|
||||||
|
|
||||||
|
if [[ $secure_boot == "auto" ]]; then
|
||||||
|
if usb_authorization_secure_boot_enabled; then
|
||||||
|
secure_boot=enabled
|
||||||
|
else
|
||||||
|
secure_boot=disabled
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r $config ]]; then
|
||||||
|
echo "Cannot verify the generated Limine boot entries in $config." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
paths=$(mktemp /tmp/omarchy-usb-boot-paths.XXXXXXXXXX)
|
||||||
|
section=$(mktemp /tmp/omarchy-usb-boot-cmdline.XXXXXXXXXX)
|
||||||
|
|
||||||
|
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
||||||
|
function contains_parameter(value, count, fields, field_index) {
|
||||||
|
count = split(value, fields, /[[:space:]]+/)
|
||||||
|
for (field_index = 1; field_index <= count; field_index++) {
|
||||||
|
if (fields[field_index] == parameter) return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish_entry(found) {
|
||||||
|
if (!target_os || !is_kernel) return
|
||||||
|
entries++
|
||||||
|
|
||||||
|
if (has_cmdline) {
|
||||||
|
found = contains_parameter(cmdline)
|
||||||
|
if ((expected == "enabled" && !found) || (expected == "disabled" && found)) bad = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path == "") {
|
||||||
|
bad = 1
|
||||||
|
} else {
|
||||||
|
print (has_cmdline ? "external" : "embedded") "\t" path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\/[^/]/ {
|
||||||
|
finish_entry()
|
||||||
|
target_os = 0
|
||||||
|
is_kernel = 0
|
||||||
|
has_cmdline = 0
|
||||||
|
cmdline = ""
|
||||||
|
path = ""
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
||||||
|
target_os = index($0, "machine-id=" machine_id) > 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[[:space:]]*\/\// {
|
||||||
|
finish_entry()
|
||||||
|
is_kernel = 0
|
||||||
|
has_cmdline = 0
|
||||||
|
cmdline = ""
|
||||||
|
path = ""
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[[:space:]]*comment:[[:space:]]*kernel-id=/ {
|
||||||
|
is_kernel = target_os
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
is_kernel && /^[[:space:]]*path:[[:space:]]*/ {
|
||||||
|
path = $0
|
||||||
|
sub(/^[[:space:]]*path:[[:space:]]*/, "", path)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
is_kernel && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
||||||
|
has_cmdline = 1
|
||||||
|
cmdline = $0
|
||||||
|
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", cmdline)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
finish_entry()
|
||||||
|
if (entries == 0) bad = 1
|
||||||
|
exit bad
|
||||||
|
}
|
||||||
|
' "$config" >"$paths"; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "The generated Limine kernel entries do not all have USB authorization $expected." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS=$'\t' read -r mode path; do
|
||||||
|
if [[ $path != 'boot():/'* ]]; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "Cannot verify the kernel image referenced by Limine: $path" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
image="$esp/${path#'boot():/'}"
|
||||||
|
image=${image%%#*}
|
||||||
|
if [[ ! -r $image ]]; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "Cannot verify the kernel image referenced by Limine: $image" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $path == *'#'* ]]; then
|
||||||
|
expected_hash=${path##*#}
|
||||||
|
actual_hash=$(b2sum "$image")
|
||||||
|
actual_hash=${actual_hash%% *}
|
||||||
|
if [[ $actual_hash != "$expected_hash" ]]; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "The verification hash for $image does not match the generated Limine entry." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
: >"$section"
|
||||||
|
if ! objcopy -O binary --only-section=.cmdline "$image" "$section" 2>/dev/null; then
|
||||||
|
[[ $mode == "external" ]] && continue
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "Cannot inspect the embedded kernel command line in $image." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
embedded_cmdline=$(tr '\0' '\n' <"$section")
|
||||||
|
|
||||||
|
# Without Secure Boot, Limine's external command line takes precedence.
|
||||||
|
# Under Secure Boot, systemd-stub uses a nonempty embedded .cmdline.
|
||||||
|
if [[ $mode == "external" && $secure_boot == "disabled" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A signed UKI can ignore Limine's external cmdline and use its embedded
|
||||||
|
# .cmdline instead. If one exists, both representations must agree.
|
||||||
|
if [[ -z $embedded_cmdline ]]; then
|
||||||
|
if [[ $mode == "external" || $expected == "disabled" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "The generated kernel image $image has no verifiable kernel command line." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $expected == "enabled" ]]; then
|
||||||
|
if ! usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "The generated kernel image $image does not enable boot-time USB authorization." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
elif usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
echo "The generated kernel image $image still enables boot-time USB authorization." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done <"$paths"
|
||||||
|
|
||||||
|
rm -f "$paths" "$section"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_rebuild_and_verify() {
|
||||||
|
local expected="$1" config="$2" esp="$3"
|
||||||
|
local machine_id="${4:-$(</etc/machine-id)}" secure_boot="${5:-auto}"
|
||||||
|
|
||||||
|
if ! limine-mkinitcpio; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# limine-mkinitcpio-hook can swallow an individual kernel build failure and
|
||||||
|
# still return success, so its exit status alone is not a safety boundary.
|
||||||
|
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id" "$secure_boot"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_snapshot_block_status() {
|
||||||
|
local config="$1"
|
||||||
|
|
||||||
|
[[ -r $config ]] || return 2
|
||||||
|
awk -v begin="$snapshot_begin" -v setting="$snapshot_setting" -v end="$snapshot_end" '
|
||||||
|
$0 == begin { begin_count++; state = 1; next }
|
||||||
|
$0 == setting { setting_count++; if (state == 1) state = 2; next }
|
||||||
|
$0 == end { end_count++; if (state == 2) valid_count++; state = 0; next }
|
||||||
|
{ state = 0 }
|
||||||
|
END {
|
||||||
|
if (begin_count == 0 && setting_count == 0 && end_count == 0) exit 1
|
||||||
|
if (begin_count == 1 && setting_count == 1 && end_count == 1 && valid_count == 1) exit 0
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_enable_snapshot_setting() {
|
||||||
|
local config="$1" status temporary
|
||||||
|
|
||||||
|
if usb_authorization_snapshot_block_status "$config"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
if (( status != 1 )); then
|
||||||
|
echo "Refusing to modify the USB authorization block in $config." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
||||||
|
cp --preserve=mode,ownership "$config" "$temporary"
|
||||||
|
{
|
||||||
|
cat "$config"
|
||||||
|
printf '\n%s\n%s\n%s\n' "$snapshot_begin" "$snapshot_setting" "$snapshot_end"
|
||||||
|
} >"$temporary"
|
||||||
|
mv -f "$temporary" "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_disable_snapshot_setting() {
|
||||||
|
local config="$1" status temporary
|
||||||
|
|
||||||
|
if usb_authorization_snapshot_block_status "$config"; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
(( status == 1 )) && return 0
|
||||||
|
if (( status != 0 )); then
|
||||||
|
echo "Refusing to modify the USB authorization block in $config." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
||||||
|
cp --preserve=mode,ownership "$config" "$temporary"
|
||||||
|
awk -v begin="$snapshot_begin" '
|
||||||
|
$0 == begin { skip = 2; next }
|
||||||
|
skip > 0 { skip--; next }
|
||||||
|
{ print }
|
||||||
|
' "$config" >"$temporary"
|
||||||
|
mv -f "$temporary" "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_rewrite_boot_cmdlines() {
|
||||||
|
local expected="$1" config="$2" machine_id="${3:-$(</etc/machine-id)}"
|
||||||
|
local backup temporary
|
||||||
|
|
||||||
|
backup=$(mktemp "${config}.omarchy-usb-backup.XXXXXXXXXX")
|
||||||
|
temporary=$(mktemp "${config}.omarchy-usb-new.XXXXXXXXXX")
|
||||||
|
cp --preserve=mode,ownership "$config" "$backup"
|
||||||
|
cp --preserve=mode,ownership "$config" "$temporary"
|
||||||
|
|
||||||
|
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
||||||
|
function rewrite_cmdline(line, prefix, value, count, fields, field_index, result, found) {
|
||||||
|
prefix = line
|
||||||
|
sub(/cmdline:.*/, "cmdline:", prefix)
|
||||||
|
value = line
|
||||||
|
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", value)
|
||||||
|
count = split(value, fields, /[[:space:]]+/)
|
||||||
|
result = ""
|
||||||
|
found = 0
|
||||||
|
for (field_index = 1; field_index <= count; field_index++) {
|
||||||
|
if (fields[field_index] == parameter) {
|
||||||
|
found = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (fields[field_index] != "") result = result (result == "" ? "" : " ") fields[field_index]
|
||||||
|
}
|
||||||
|
if (expected == "enabled") result = result (result == "" ? "" : " ") parameter
|
||||||
|
return prefix (result == "" ? "" : " ") result
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\/[^/]/ { target_os = 0 }
|
||||||
|
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
||||||
|
target_os = index($0, "machine-id=" machine_id) > 0
|
||||||
|
}
|
||||||
|
target_os && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
||||||
|
print rewrite_cmdline($0)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' "$config" >"$temporary"; then
|
||||||
|
rm -f "$backup" "$temporary"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv -f "$temporary" "$config"
|
||||||
|
if ! limine-enroll-config; then
|
||||||
|
mv -f "$backup" "$config"
|
||||||
|
limine-enroll-config >/dev/null 2>&1 || true
|
||||||
|
echo "Restored $config because its bootloader verification data could not be updated." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
|
||||||
|
|
||||||
|
if (( EUID != 0 )); then
|
||||||
|
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
enable)
|
||||||
|
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
||||||
|
snapshot_status=0
|
||||||
|
else
|
||||||
|
snapshot_status=$?
|
||||||
|
fi
|
||||||
|
if (( snapshot_status == 2 )); then
|
||||||
|
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Quote the expected value because the Limine key contains glob brackets.
|
||||||
|
if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then
|
||||||
|
echo "Refusing to replace the existing $drop_in." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
||||||
|
|
||||||
|
if [[ ! -e $drop_in ]]; then
|
||||||
|
install -d -m755 "${drop_in%/*}"
|
||||||
|
temporary=$(mktemp "${drop_in}.XXXXXXXXXX")
|
||||||
|
trap 'rm -f "$temporary"' EXIT
|
||||||
|
printf '%s\n' "$setting" >"$temporary"
|
||||||
|
chmod 644 "$temporary"
|
||||||
|
mv -f "$temporary" "$drop_in"
|
||||||
|
trap - EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! limine-mkinitcpio ||
|
||||||
|
! usb_authorization_rewrite_boot_cmdlines enabled "$limine_conf" ||
|
||||||
|
! usb_authorization_verify_boot_entries enabled "$limine_conf" "$boot_path"; then
|
||||||
|
echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
||||||
|
had_snapshot_setting=1
|
||||||
|
else
|
||||||
|
snapshot_status=$?
|
||||||
|
if (( snapshot_status == 2 )); then
|
||||||
|
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e $drop_in ]]; then
|
||||||
|
if [[ $(<"$drop_in") != "$setting" ]]; then
|
||||||
|
echo "Refusing to remove the modified $drop_in." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
had_drop_in=1
|
||||||
|
rm -f "$drop_in"
|
||||||
|
elif (( ! had_snapshot_setting )) && usb_authorization_verify_boot_entries disabled "$limine_conf" "$boot_path"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
usb_authorization_disable_snapshot_setting "$limine_defaults"
|
||||||
|
if ! limine-mkinitcpio ||
|
||||||
|
! usb_authorization_rewrite_boot_cmdlines disabled "$limine_conf" ||
|
||||||
|
! usb_authorization_verify_boot_entries disabled "$limine_conf" "$boot_path"; then
|
||||||
|
if (( had_drop_in )); then
|
||||||
|
printf '%s\n' "$setting" >"$drop_in"
|
||||||
|
chmod 644 "$drop_in"
|
||||||
|
fi
|
||||||
|
if (( had_snapshot_setting )); then
|
||||||
|
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
||||||
|
fi
|
||||||
|
echo "USBGuard remains required because the boot images could not be verified as default-allow." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:hidden=true
|
||||||
|
# omarchy:summary=Create an approval request for a blocked USB device
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[[ ${USBGUARD_IPC_SIGNAL:-} == "Device.PresenceChanged" ]] || exit 0
|
||||||
|
[[ ${USBGUARD_DEVICE_EVENT:-} == "Insert" ]] || exit 0
|
||||||
|
[[ ${USBGUARD_DEVICE_TARGET:-} == "block" ]] || exit 0
|
||||||
|
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
|
||||||
|
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
|
||||||
|
|
||||||
|
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
|
||||||
|
mkdir -p "$requests"
|
||||||
|
chmod 700 "${requests%/*}" "$requests"
|
||||||
|
|
||||||
|
# The token is safe to carry through the presentation launcher's command
|
||||||
|
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
|
||||||
|
# is never interpolated into a shell command.
|
||||||
|
token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" | sha256sum | cut -d' ' -f1)"
|
||||||
|
request="$requests/$token.json"
|
||||||
|
temporary=$(mktemp "$requests/.request.XXXXXXXXXX")
|
||||||
|
trap 'rm -f "$temporary"' EXIT
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg id "$USBGUARD_DEVICE_ID" \
|
||||||
|
--arg rule "$USBGUARD_DEVICE_RULE" \
|
||||||
|
'{id: $id, rule: $rule}' >"$temporary"
|
||||||
|
chmod 600 "$temporary"
|
||||||
|
mv -f "$temporary" "$request"
|
||||||
|
|
||||||
|
omarchy-notification-send \
|
||||||
|
--app-name omarchy-action \
|
||||||
|
--urgency critical \
|
||||||
|
--glyph \
|
||||||
|
"USB accessory blocked" \
|
||||||
|
"Click to review it. Keep it blocked unless you recognize the device." \
|
||||||
|
--exec omarchy-launch-floating-terminal-with-presentation \
|
||||||
|
omarchy-usb-authorization-review "$token"
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Restore the kernel's default USB authorization state
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if (( EUID != 0 )); then
|
||||||
|
echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# USBGuard intentionally leaves root hubs in default-deny mode when it exits.
|
||||||
|
# Restore both the hub defaults and devices that are still connected.
|
||||||
|
shopt -s nullglob
|
||||||
|
for attribute in /sys/bus/usb/devices/usb*/authorized_default; do
|
||||||
|
echo 1 >"$attribute"
|
||||||
|
done
|
||||||
|
for attribute in /sys/bus/usb/devices/*/authorized; do
|
||||||
|
echo 1 >"$attribute"
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:hidden=true
|
||||||
|
# omarchy:summary=Review a blocked USB device authorization request
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
token="${1:-}"
|
||||||
|
if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
|
||||||
|
echo "This USB authorization request is invalid or expired." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
|
||||||
|
if [[ ! -f $request || -L $request ]]; then
|
||||||
|
echo "This USB authorization request is no longer available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
id=$(jq -er '.id | select(test("^[0-9]+$"))' "$request")
|
||||||
|
expected_rule=$(jq -er '.rule | select(type == "string" and length > 0)' "$request")
|
||||||
|
read_blocked_rule() {
|
||||||
|
local line
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ $line == "$id: "* ]]; then
|
||||||
|
printf '%s\n' "${line#"$id: "}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(usbguard list-devices --blocked)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
current_rule=$(read_blocked_rule || true)
|
||||||
|
|
||||||
|
if [[ -z $current_rule || $current_rule != "$expected_rule" ]]; then
|
||||||
|
rm -f "$request"
|
||||||
|
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gum style --foreground 212 --bold "Blocked USB accessory"
|
||||||
|
echo
|
||||||
|
echo "The following details come from the device and can be forged:"
|
||||||
|
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
|
||||||
|
echo
|
||||||
|
|
||||||
|
choice=$(gum choose \
|
||||||
|
"Allow once" \
|
||||||
|
"Always allow this device" \
|
||||||
|
"Keep blocked") || exit 130
|
||||||
|
|
||||||
|
if [[ $choice != "Keep blocked" ]]; then
|
||||||
|
current_rule=$(read_blocked_rule || true)
|
||||||
|
if [[ -z $current_rule || $current_rule != "$expected_rule" ]]; then
|
||||||
|
rm -f "$request"
|
||||||
|
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
"Allow once")
|
||||||
|
# Target the complete identity rule instead of USBGuard's numeric ID. IDs can
|
||||||
|
# be reassigned when the daemon restarts while the review dialog is open.
|
||||||
|
usbguard allow-device "$expected_rule"
|
||||||
|
echo "USB accessory allowed until it is disconnected."
|
||||||
|
;;
|
||||||
|
"Always allow this device")
|
||||||
|
usbguard allow-device --permanent "$expected_rule"
|
||||||
|
echo "USB accessory added to the trusted policy."
|
||||||
|
;;
|
||||||
|
"Keep blocked")
|
||||||
|
echo "USB accessory remains blocked."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
rm -f "$request"
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:hidden=true
|
||||||
|
# omarchy:summary=Watch USBGuard for devices awaiting user approval
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
|
||||||
|
|
||||||
|
# A device inserted while the graphical session was stopped is already safely
|
||||||
|
# blocked by USBGuard. Surface it now so the user can approve it after login.
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then
|
||||||
|
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
|
||||||
|
USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \
|
||||||
|
USBGUARD_DEVICE_EVENT=Insert \
|
||||||
|
USBGUARD_DEVICE_TARGET=block \
|
||||||
|
USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \
|
||||||
|
"$event_handler"
|
||||||
|
fi
|
||||||
|
done < <(usbguard list-devices --blocked)
|
||||||
|
|
||||||
|
exec usbguard watch --wait --exec "$event_handler"
|
||||||
@@ -181,6 +181,8 @@
|
|||||||
"setup.config": {"icon":"","label":"Config"},
|
"setup.config": {"icon":"","label":"Config"},
|
||||||
"setup.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
|
"setup.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
|
||||||
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
||||||
|
"setup.security.usb-authorization": {"icon":"","label":"USB Device Authorization","checked":"systemctl is-enabled --quiet usbguard.service && systemctl --user is-enabled --quiet omarchy-usb-authorization.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-usb-authorization"},
|
||||||
|
"setup.security.usb-authorization-boot": {"icon":"","label":"USB at Boot","checked":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-security-usb-authorization --boot'"},
|
||||||
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
||||||
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
||||||
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
||||||
@@ -298,6 +300,8 @@
|
|||||||
"remove.security": {"icon":"","label":"Security","title":"Remove"},
|
"remove.security": {"icon":"","label":"Security","title":"Remove"},
|
||||||
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
||||||
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
||||||
|
"remove.security.usb-authorization": {"icon":"","label":"USB Device Authorization","when":"systemctl is-enabled --quiet usbguard.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-usb-authorization"},
|
||||||
|
"remove.security.usb-authorization-boot": {"icon":"","label":"USB at Boot","when":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-security-usb-authorization --boot-only'"},
|
||||||
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
||||||
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
||||||
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ask before allowing new USB devices
|
||||||
|
After=graphical-session.target dbus.socket
|
||||||
|
Requires=dbus.socket
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
ConditionEnvironment=OMARCHY_PATH
|
||||||
|
ConditionEnvironment=WAYLAND_DISPLAY
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecCondition=/usr/bin/systemctl is-active --quiet usbguard.service
|
||||||
|
ExecStart=/usr/bin/bash -lc 'exec "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch"'
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
@@ -7,5 +7,6 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh"
|
|||||||
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
|
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
||||||
|
run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
echo "Configure USB device authorization"
|
||||||
|
|
||||||
|
rules_file="${OMARCHY_USB_AUTHORIZATION_RULES_FILE:-/etc/usbguard/rules.conf}"
|
||||||
|
daemon_config="${OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG:-/etc/usbguard/usbguard-daemon.conf}"
|
||||||
|
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
|
||||||
|
usb_authorization_add_user "$OMARCHY_INSTALL_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installs are followed by a reboot, so enable the daemon without starting it
|
||||||
|
# in the live ISO's chroot.
|
||||||
|
systemctl enable usbguard.service
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
usb_authorization_config_value() {
|
||||||
|
local config="$1" key="$2"
|
||||||
|
local awk_command=(awk)
|
||||||
|
|
||||||
|
if [[ ! -r $config ]]; then
|
||||||
|
if [[ $config == "/etc/usbguard/usbguard-daemon.conf" ]]; then
|
||||||
|
awk_command=(sudo awk)
|
||||||
|
else
|
||||||
|
echo "Cannot read USBGuard configuration: $config" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${awk_command[@]}" -F= -v key="$key" '
|
||||||
|
/^[[:space:]]*#/ { next }
|
||||||
|
$1 ~ "^[[:space:]]*" key "[[:space:]]*$" {
|
||||||
|
value=$0
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "", value)
|
||||||
|
sub(/[[:space:]]*$/, "", value)
|
||||||
|
print value
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_rules_present() {
|
||||||
|
local rules="$1"
|
||||||
|
|
||||||
|
if [[ -r $rules ]]; then
|
||||||
|
[[ -s $rules ]]
|
||||||
|
elif [[ $rules == "/etc/usbguard/rules.conf" ]]; then
|
||||||
|
sudo test -s "$rules"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_require_secure_setting() {
|
||||||
|
local config="$1" key="$2" expected="$3" actual
|
||||||
|
|
||||||
|
actual=$(usb_authorization_config_value "$config" "$key")
|
||||||
|
if [[ $actual != "$expected" ]]; then
|
||||||
|
echo "USBGuard's $key must be '$expected' before Omarchy enables it." >&2
|
||||||
|
echo "Current value: ${actual:-<unset>} in $config" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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_generate_policy() {
|
||||||
|
local policy="$1"
|
||||||
|
|
||||||
|
usbguard generate-policy >"$policy"
|
||||||
|
if [[ ! -s $policy ]] || ! grep -q '^allow ' "$policy"; then
|
||||||
|
echo "USBGuard did not generate a usable policy; nothing was enabled." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_add_user() {
|
||||||
|
local user="$1"
|
||||||
|
|
||||||
|
# This user can inspect USBGuard events and approve the device named by an
|
||||||
|
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
|
||||||
|
usbguard add-user "$user" \
|
||||||
|
--devices=list,listen,modify \
|
||||||
|
--policy=list \
|
||||||
|
--exceptions=listen
|
||||||
|
}
|
||||||
|
|
||||||
|
usb_authorization_trust_present_devices() {
|
||||||
|
local line id
|
||||||
|
local count=0
|
||||||
|
local -a devices
|
||||||
|
|
||||||
|
mapfile -t devices < <(usbguard list-devices)
|
||||||
|
|
||||||
|
for line in "${devices[@]}"; do
|
||||||
|
if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then
|
||||||
|
id="${BASH_REMATCH[1]}"
|
||||||
|
usbguard allow-device --permanent "$id"
|
||||||
|
(( ++count ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( count == 0 )); then
|
||||||
|
echo "USBGuard did not report any connected USB devices; boot authorization was not enabled." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -133,6 +133,7 @@ udiskie
|
|||||||
ufw
|
ufw
|
||||||
ufw-docker
|
ufw-docker
|
||||||
unzip
|
unzip
|
||||||
|
usbguard
|
||||||
usage
|
usage
|
||||||
uwsm
|
uwsm
|
||||||
vi
|
vi
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ systemctl --user enable --now \
|
|||||||
omarchy-sleep-lock.service \
|
omarchy-sleep-lock.service \
|
||||||
omarchy-migrate-notify.service \
|
omarchy-migrate-notify.service \
|
||||||
omarchy-fcitx5.service \
|
omarchy-fcitx5.service \
|
||||||
omarchy-crash-watch.service
|
omarchy-crash-watch.service \
|
||||||
|
omarchy-usb-authorization.service
|
||||||
+14
-1
@@ -6,7 +6,20 @@ Omarchy takes security extremely seriously. This is meant to be an operating sys
|
|||||||
2. *Firewall is enabled by default*: All incoming traffic is blocked by default except for port 53317 for [LocalSend](https://localsend.org/). Even ssh is off until you turn it on via _Setup > Security > SSHD_, which opens port 22 (rate limited against brute force) as part of the setup. We even lock down Docker access using the [ufw-docker](https://github.com/chaifeng/ufw-docker) setup to prevent that your containers are accidentally exposed to the world.
|
2. *Firewall is enabled by default*: All incoming traffic is blocked by default except for port 53317 for [LocalSend](https://localsend.org/). Even ssh is off until you turn it on via _Setup > Security > SSHD_, which opens port 22 (rate limited against brute force) as part of the setup. We even lock down Docker access using the [ufw-docker](https://github.com/chaifeng/ufw-docker) setup to prevent that your containers are accidentally exposed to the world.
|
||||||
3. *Arch always have the latest updates*: Arch, the underlying distro that Omarchy is built on, is a rolling distribution. This means that any security vulnerability that's discovered and patched in any package is quickly available for install using `omarchy-update`. You're always running the latest, most secure versions of everything that way.
|
3. *Arch always have the latest updates*: Arch, the underlying distro that Omarchy is built on, is a rolling distribution. This means that any security vulnerability that's discovered and patched in any package is quickly available for install using `omarchy-update`. You're always running the latest, most secure versions of everything that way.
|
||||||
4. *Omarchy maintains its own packages and mirror*: Omarchy only relies on packages from Arch's own core/extra/multilib repositories and its own Omarchy Package Repository by default. You can install software directly from AUR, but the base install doesn't — only a few optional installs, like the third-party browsers, pull from the AUR.
|
4. *Omarchy maintains its own packages and mirror*: Omarchy only relies on packages from Arch's own core/extra/multilib repositories and its own Omarchy Package Repository by default. You can install software directly from AUR, but the base install doesn't — only a few optional installs, like the third-party browsers, pull from the AUR.
|
||||||
5. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability.
|
5. *New USB devices require approval*: Omarchy trusts the USB devices present during installation, then blocks newly connected accessories until you approve them. This keeps an unknown accessory from reaching a matching kernel driver merely because it claims a supported device identity.
|
||||||
|
6. *Cloudflare protects us from DDoS*: All the Omarchy distribution infrastructure — the ISOs, the Omarchy packages, the Arch mirror — is protected behind Cloudflare's formidable DDoS shield and hosted on their CDN. This provides superb availability.
|
||||||
|
|
||||||
|
## USB Device Authorization
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The policy protects devices connected after USBGuard starts during boot. It does not replace operating-system and driver security updates, and USB devices needed before the service starts, such as a keyboard used to unlock an encrypted disk, remain governed by the kernel's boot-time policy.
|
||||||
|
|
||||||
|
For machines whose disk unlock and recovery never depend on USB input, _Setup > Security > USB at Boot_ can extend the block to the beginning of kernel boot. Omarchy adds every currently connected device to USBGuard's trusted policy, sets `usbcore.authorized_default=0`, and updates current and Limine snapshot boot entries. The trusted policy applies only once USBGuard starts: even a trusted USB keyboard remains unavailable at an earlier encrypted-disk password prompt. _Remove > Security > USB at Boot_ restores the normal early-boot behavior while keeping USBGuard active after startup.
|
||||||
|
|
||||||
## Changing your passwords
|
## Changing your passwords
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
echo "Enable approval for newly connected USB devices"
|
||||||
|
|
||||||
|
omarchy-setup-security-usb-authorization --yes
|
||||||
@@ -324,6 +324,13 @@ assert(
|
|||||||
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
|
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
|
||||||
'menu places Passwordless Sudo under Setup > Security'
|
'menu places Passwordless Sudo under Setup > Security'
|
||||||
)
|
)
|
||||||
|
assert(
|
||||||
|
defaultById['setup.security.usb-authorization'].action.includes('omarchy-setup-security-usb-authorization')
|
||||||
|
&& defaultById['remove.security.usb-authorization'].action.includes('omarchy-remove-security-usb-authorization')
|
||||||
|
&& defaultById['setup.security.usb-authorization-boot'].action.includes('omarchy-setup-security-usb-authorization --boot')
|
||||||
|
&& defaultById['remove.security.usb-authorization-boot'].action.includes('omarchy-remove-security-usb-authorization --boot-only'),
|
||||||
|
'menu can enable and remove USB device authorization under Security'
|
||||||
|
)
|
||||||
assert(
|
assert(
|
||||||
!defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'],
|
!defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'],
|
||||||
'menu removes the relocated toggles from Trigger > Toggle'
|
'menu removes the relocated toggles from Trigger > Toggle'
|
||||||
|
|||||||
Executable
+405
@@ -0,0 +1,405 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
scratch=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$scratch"' EXIT
|
||||||
|
|
||||||
|
stub_bin="$scratch/bin"
|
||||||
|
home="$scratch/home"
|
||||||
|
rules="$scratch/etc/usbguard/rules.conf"
|
||||||
|
daemon_config="$scratch/etc/usbguard/usbguard-daemon.conf"
|
||||||
|
calls="$scratch/calls"
|
||||||
|
|
||||||
|
mkdir -p "$stub_bin" "$home" "${rules%/*}"
|
||||||
|
: >"$rules"
|
||||||
|
: >"$calls"
|
||||||
|
|
||||||
|
cat >"$daemon_config" <<'CONF'
|
||||||
|
ImplicitPolicyTarget=block
|
||||||
|
PresentDevicePolicy=apply-policy
|
||||||
|
InsertedDevicePolicy=apply-policy
|
||||||
|
AuthorizedDefault=none
|
||||||
|
RestoreControllerDeviceState=false
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >"$stub_bin/omarchy-pkg-missing" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/omarchy-pkg-present" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/omarchy-pkg-add" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'pkg-add <%s>\n' "$*" >>"$CALLS"
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/omarchy-pkg-drop" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'pkg-drop <%s>\n' "$*" >>"$CALLS"
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/install" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
if [[ $1 == -Dm600 && $2 == -o && $3 == root && $4 == -g && $5 == root ]]; then
|
||||||
|
exec /usr/bin/install -Dm600 "$6" "$7"
|
||||||
|
fi
|
||||||
|
exec /usr/bin/install "$@"
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/systemctl" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'systemctl <%s>\n' "$*" >>"$CALLS"
|
||||||
|
if [[ $* == "is-enabled --quiet usbguard.service" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/usbguard" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
case "$1" in
|
||||||
|
generate-policy)
|
||||||
|
echo 'usbguard <generate-policy>' >>"$CALLS"
|
||||||
|
echo 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"'
|
||||||
|
;;
|
||||||
|
list-devices)
|
||||||
|
if [[ ${2:-} == "--blocked" ]]; then
|
||||||
|
if [[ ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
|
||||||
|
rule="${BLOCKED_DEVICE_RULE:?}"
|
||||||
|
if [[ -n ${BLOCKED_DEVICE_RULE_FILE:-} && -f $BLOCKED_DEVICE_RULE_FILE ]]; then
|
||||||
|
rule=$(<"$BLOCKED_DEVICE_RULE_FILE")
|
||||||
|
fi
|
||||||
|
printf '17: %s\n' "$rule"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo '4: allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"'
|
||||||
|
echo '5: allow id 0627:0001 name "QEMU USB Tablet" hash "tablet"'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'usbguard' >>"$CALLS"
|
||||||
|
printf ' <%s>' "$@" >>"$CALLS"
|
||||||
|
printf '\n' >>"$CALLS"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/sudo" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'sudo' >>"$CALLS"
|
||||||
|
printf ' <%s>' "$@" >>"$CALLS"
|
||||||
|
printf '\n' >>"$CALLS"
|
||||||
|
if [[ $1 == awk && ${!#} == /etc/usbguard/usbguard-daemon.conf ]]; then
|
||||||
|
args=("$@")
|
||||||
|
args[-1]="$TEST_DAEMON_CONFIG"
|
||||||
|
exec "${args[@]}"
|
||||||
|
elif [[ $1 == test && $2 == -s && $3 == /etc/usbguard/rules.conf ]]; then
|
||||||
|
exec /usr/bin/test -s "$TEST_RULES"
|
||||||
|
elif [[ $1 == install && $2 == -Dm600 && $3 == -o && $4 == root && $5 == -g && $6 == root && $8 == /etc/usbguard/rules.conf ]]; then
|
||||||
|
exec /usr/bin/install -Dm600 "$7" "$TEST_RULES"
|
||||||
|
elif [[ $1 == omarchy-usb-authorization-restore-default ]]; then
|
||||||
|
echo 1 >"$TEST_SYSFS/usb1/authorized_default"
|
||||||
|
echo 1 >"$TEST_SYSFS/1-2/authorized"
|
||||||
|
exit 0
|
||||||
|
elif [[ $1 == omarchy-usb-authorization-boot ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/gum" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
case "$1" in
|
||||||
|
style) exit 0 ;;
|
||||||
|
choose)
|
||||||
|
if [[ -n ${GUM_REASSIGN_RULE:-} ]]; then
|
||||||
|
printf '%s\n' "$GUM_REASSIGN_RULE" >"$BLOCKED_DEVICE_RULE_FILE"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${GUM_CHOICE:-Keep blocked}"
|
||||||
|
;;
|
||||||
|
confirm) exit 0 ;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
cat >"$stub_bin/omarchy-notification-send" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'notification' >>"$CALLS"
|
||||||
|
printf ' <%s>' "$@" >>"$CALLS"
|
||||||
|
printf '\n' >>"$CALLS"
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_bin"/*
|
||||||
|
|
||||||
|
export HOME="$home"
|
||||||
|
export USER=tester
|
||||||
|
export OMARCHY_PATH="$ROOT"
|
||||||
|
export CALLS="$calls"
|
||||||
|
export PATH="$stub_bin:$ROOT/bin:$PATH"
|
||||||
|
export TEST_DAEMON_CONFIG="$daemon_config"
|
||||||
|
export TEST_RULES="$rules"
|
||||||
|
|
||||||
|
if "$ROOT/bin/omarchy-usb-authorization-restore-default" >/dev/null 2>&1; then
|
||||||
|
fail "the fixed-path USB authorization restore helper requires root"
|
||||||
|
fi
|
||||||
|
if "$ROOT/bin/omarchy-usb-authorization-boot" enable >/dev/null 2>&1; then
|
||||||
|
fail "the fixed-path boot authorization helper requires root"
|
||||||
|
fi
|
||||||
|
! grep -q 'OMARCHY_USB_AUTHORIZATION_.*ROOT\|OMARCHY_USB_AUTHORIZATION_.*FILE\|OMARCHY_USB_AUTHORIZATION_.*CONFIG' \
|
||||||
|
"$ROOT/bin/omarchy-setup-security-usb-authorization" \
|
||||||
|
"$ROOT/bin/omarchy-remove-security-usb-authorization" \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-boot" ||
|
||||||
|
fail "user-facing USB authorization commands do not elevate caller-selected paths"
|
||||||
|
pass "USB authorization keeps privileged paths fixed"
|
||||||
|
|
||||||
|
"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-output"
|
||||||
|
|
||||||
|
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' "$rules" ||
|
||||||
|
fail "USB authorization setup trusts devices present during enrollment"
|
||||||
|
[[ $(stat -c %a "$rules") == 600 ]] || fail "USB authorization policy is root-private"
|
||||||
|
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
|
||||||
|
fail "USB authorization grants only the IPC access needed by the approval flow"
|
||||||
|
grep -Fqx 'systemctl <restart usbguard.service>' "$calls" ||
|
||||||
|
fail "USB authorization reloads its user ACL before the watcher starts"
|
||||||
|
[[ -L $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
|
||||||
|
fail "USB authorization installs the graphical-session watcher"
|
||||||
|
grep -Fqx 'pkg-add <usbguard>' "$calls" || fail "USB authorization ensures USBGuard is installed"
|
||||||
|
pass "USB authorization enrolls present devices before enabling default-deny"
|
||||||
|
|
||||||
|
generate_count=$(grep -c '^usbguard <generate-policy>$' "$calls")
|
||||||
|
"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-existing-output"
|
||||||
|
generate_count_after=$(grep -c '^usbguard <generate-policy>$' "$calls")
|
||||||
|
(( generate_count_after == generate_count )) || fail "re-enabling preserves the existing trusted-device policy"
|
||||||
|
pass "USB authorization setup preserves an existing policy"
|
||||||
|
|
||||||
|
: >"$calls"
|
||||||
|
"$ROOT/bin/omarchy-setup-security-usb-authorization" --boot --yes >"$scratch/setup-boot-output"
|
||||||
|
grep -Fqx 'usbguard <allow-device> <--permanent> <4>' "$calls" ||
|
||||||
|
fail "boot authorization trusts the connected root controller"
|
||||||
|
grep -Fqx 'usbguard <allow-device> <--permanent> <5>' "$calls" ||
|
||||||
|
fail "boot authorization trusts the connected USB devices"
|
||||||
|
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <enable>' "$calls" ||
|
||||||
|
fail "boot authorization uses the fixed-path boot-image helper"
|
||||||
|
grep -Fq 'takes effect after reboot' "$scratch/setup-boot-output" ||
|
||||||
|
fail "boot authorization explains when the change becomes active"
|
||||||
|
pass "USB authorization can snapshot connected devices for early-boot denial"
|
||||||
|
|
||||||
|
boot_conf="$scratch/limine.conf"
|
||||||
|
machine_id=$(</etc/machine-id)
|
||||||
|
touch "$scratch/vmlinuz-linux"
|
||||||
|
boot_hash=$(b2sum "$scratch/vmlinuz-linux")
|
||||||
|
boot_hash=${boot_hash%% *}
|
||||||
|
printf 'quiet splash\0' >"$scratch/embedded-cmdline"
|
||||||
|
historical_uki="$scratch/embedded.efi_sha256_fixture"
|
||||||
|
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
|
||||||
|
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
|
||||||
|
embedded_hash=$(b2sum "$historical_uki")
|
||||||
|
embedded_hash=${embedded_hash%% *}
|
||||||
|
cat >"$boot_conf" <<CONF
|
||||||
|
/+Omarchy
|
||||||
|
comment: machine-id=$machine_id
|
||||||
|
//linux
|
||||||
|
comment: kernel-id=linux
|
||||||
|
path: boot():/vmlinuz-linux#$boot_hash
|
||||||
|
cmdline: quiet splash
|
||||||
|
//linux-embedded
|
||||||
|
comment: kernel-id=linux-embedded
|
||||||
|
path: boot():/embedded.efi_sha256_fixture#$embedded_hash
|
||||||
|
cmdline: quiet splash
|
||||||
|
///Snapshots
|
||||||
|
////linux
|
||||||
|
comment: kernel-id=linux
|
||||||
|
path: boot():/vmlinuz-linux#$boot_hash
|
||||||
|
cmdline: rootflags=subvol=/snapshot quiet splash
|
||||||
|
/Other Linux
|
||||||
|
comment: machine-id=ffffffffffffffffffffffffffffffff
|
||||||
|
//linux
|
||||||
|
comment: kernel-id=linux
|
||||||
|
path: boot():/other-linux
|
||||||
|
cmdline: quiet splash
|
||||||
|
CONF
|
||||||
|
|
||||||
|
source "$ROOT/bin/omarchy-usb-authorization-boot"
|
||||||
|
limine-mkinitcpio() { return 0; }
|
||||||
|
limine-enroll-config() { return 0; }
|
||||||
|
|
||||||
|
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
|
||||||
|
fail "boot authorization rejects Limine's false-success status when entries were not rebuilt"
|
||||||
|
fi
|
||||||
|
usb_authorization_rewrite_boot_cmdlines enabled "$boot_conf" "$machine_id"
|
||||||
|
usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" disabled ||
|
||||||
|
fail "a non-Secure-Boot snapshot uses its updated external command line"
|
||||||
|
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
|
||||||
|
fail "an external allow parameter cannot hide a stale embedded UKI command line"
|
||||||
|
fi
|
||||||
|
printf 'quiet splash usbcore.authorized_default=0\0' >"$scratch/embedded-cmdline"
|
||||||
|
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
|
||||||
|
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
|
||||||
|
updated_embedded_hash=$(b2sum "$historical_uki")
|
||||||
|
updated_embedded_hash=${updated_embedded_hash%% *}
|
||||||
|
sed -i "s/embedded.efi_sha256_fixture#$embedded_hash/embedded.efi_sha256_fixture#$updated_embedded_hash/" "$boot_conf"
|
||||||
|
usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled ||
|
||||||
|
fail "boot authorization updates current and snapshot entries for this machine"
|
||||||
|
[[ $(grep -c 'usbcore.authorized_default=0' "$boot_conf") == 3 ]] ||
|
||||||
|
fail "boot authorization leaves another operating system's kernel entries unchanged"
|
||||||
|
printf 'stale image' >"$scratch/vmlinuz-linux"
|
||||||
|
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
|
||||||
|
fail "boot authorization rejects a stale Limine verification hash"
|
||||||
|
fi
|
||||||
|
: >"$scratch/vmlinuz-linux"
|
||||||
|
if usb_authorization_rebuild_and_verify disabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
|
||||||
|
fail "boot authorization detects a stale deny-by-default boot entry during removal"
|
||||||
|
fi
|
||||||
|
usb_authorization_rewrite_boot_cmdlines disabled "$boot_conf" "$machine_id"
|
||||||
|
printf 'quiet splash\0' >"$scratch/embedded-cmdline"
|
||||||
|
objcopy -I binary -O pei-x86-64 -B i386:x86-64 "$scratch/embedded-cmdline" "$historical_uki"
|
||||||
|
objcopy --rename-section .data=.cmdline,alloc,load,readonly,data,contents "$historical_uki"
|
||||||
|
disabled_embedded_hash=$(b2sum "$historical_uki")
|
||||||
|
disabled_embedded_hash=${disabled_embedded_hash%% *}
|
||||||
|
sed -i "s/embedded.efi_sha256_fixture#$updated_embedded_hash/embedded.efi_sha256_fixture#$disabled_embedded_hash/" "$boot_conf"
|
||||||
|
usb_authorization_rebuild_and_verify disabled "$boot_conf" "$scratch" "$machine_id" enabled ||
|
||||||
|
fail "boot authorization removes the parameter from current and snapshot entries"
|
||||||
|
|
||||||
|
snapshot_config="$scratch/limine-defaults"
|
||||||
|
printf 'TARGET_OS_NAME="Omarchy"\n' >"$snapshot_config"
|
||||||
|
usb_authorization_enable_snapshot_setting "$snapshot_config"
|
||||||
|
grep -Fqx 'SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0' "$snapshot_config" ||
|
||||||
|
fail "future Limine snapshots inherit boot-time USB authorization"
|
||||||
|
usb_authorization_enable_snapshot_setting "$snapshot_config"
|
||||||
|
[[ $(grep -Fxc '# Omarchy USB authorization begin' "$snapshot_config") == 1 ]] ||
|
||||||
|
fail "the snapshot setting remains idempotent"
|
||||||
|
usb_authorization_disable_snapshot_setting "$snapshot_config"
|
||||||
|
! grep -Fq 'usbcore.authorized_default=0' "$snapshot_config" ||
|
||||||
|
fail "removing boot authorization restores future snapshot defaults"
|
||||||
|
pass "boot authorization verifies generated entries despite Limine false-success exits"
|
||||||
|
|
||||||
|
: >"$calls"
|
||||||
|
"$ROOT/bin/omarchy-remove-security-usb-authorization" --boot-only --yes >"$scratch/remove-boot-output"
|
||||||
|
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <disable>' "$calls" ||
|
||||||
|
fail "boot-only removal rebuilds the permissive boot policy"
|
||||||
|
! grep -Fq 'systemctl <--user disable --now omarchy-usb-authorization.service>' "$calls" ||
|
||||||
|
fail "boot-only removal leaves the userspace approval watcher enabled"
|
||||||
|
grep -Fq 'takes effect after reboot' "$scratch/remove-boot-output" ||
|
||||||
|
fail "boot-only removal explains when the change becomes active"
|
||||||
|
pass "early-boot denial can be removed without disabling USBGuard"
|
||||||
|
|
||||||
|
grep -qx 'usbguard' "$ROOT/install/omarchy-base.packages" || fail "USBGuard is installed by default"
|
||||||
|
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 -Fqx 'omarchy-setup-security-usb-authorization --yes' "$ROOT/migrations/1789433473.sh" ||
|
||||||
|
fail "existing installs enable USB authorization during migration"
|
||||||
|
|
||||||
|
: >"$rules"
|
||||||
|
: >"$calls"
|
||||||
|
export OMARCHY_INSTALL="$ROOT/install"
|
||||||
|
export OMARCHY_INSTALL_USER=tester
|
||||||
|
export OMARCHY_USB_AUTHORIZATION_RULES_FILE="$rules"
|
||||||
|
export OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG="$daemon_config"
|
||||||
|
bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-output"
|
||||||
|
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' "$rules" ||
|
||||||
|
fail "fresh installation enrolls devices present during installation"
|
||||||
|
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
|
||||||
|
fail "fresh installation grants the owner narrowly scoped approval access"
|
||||||
|
grep -Fqx 'systemctl <enable usbguard.service>' "$calls" ||
|
||||||
|
fail "fresh installation enables USBGuard for the first boot"
|
||||||
|
pass "USB authorization is the default for fresh and existing installations"
|
||||||
|
|
||||||
|
malicious_rule='block id 07a6:8513 name "$(touch '"$scratch"'/injected)" hash "attacker" with-interface 02:06:00'
|
||||||
|
export BLOCKED_DEVICE_RULE="$malicious_rule"
|
||||||
|
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
|
||||||
|
USBGUARD_DEVICE_ID=17 \
|
||||||
|
USBGUARD_DEVICE_EVENT=Insert \
|
||||||
|
USBGUARD_DEVICE_TARGET=block \
|
||||||
|
USBGUARD_DEVICE_RULE="$malicious_rule" \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
|
||||||
|
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
|
||||||
|
[[ -n $request ]] || fail "a blocked USB device creates a review request"
|
||||||
|
[[ $(stat -c %a "$request") == 600 ]] || fail "device-controlled request data stays private"
|
||||||
|
[[ $(jq -r .id "$request") == 17 ]] || fail "the request records the USBGuard device id"
|
||||||
|
[[ $(jq -r .rule "$request") == "$malicious_rule" ]] || fail "the request preserves the identity snapshot"
|
||||||
|
! grep -Fq "$malicious_rule" "$calls" || fail "device text never enters the notification's launch command"
|
||||||
|
[[ ! -e $scratch/injected ]] || fail "device text cannot execute during notification"
|
||||||
|
token=$(basename "${request%.json}")
|
||||||
|
grep -Eq "notification .*<omarchy-usb-authorization-review> <$token>$" "$calls" ||
|
||||||
|
fail "the notification action carries only an opaque request token"
|
||||||
|
pass "blocked device metadata remains data across the desktop notification boundary"
|
||||||
|
|
||||||
|
GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-once-output"
|
||||||
|
grep -Fqx "usbguard <allow-device> <$malicious_rule>" "$calls" || fail "review can allow the exact device once"
|
||||||
|
[[ ! -e $request ]] || fail "a completed review consumes its request"
|
||||||
|
pass "USB review allows a still-matching device once"
|
||||||
|
|
||||||
|
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
|
||||||
|
USBGUARD_DEVICE_ID=17 \
|
||||||
|
USBGUARD_DEVICE_EVENT=Insert \
|
||||||
|
USBGUARD_DEVICE_TARGET=block \
|
||||||
|
USBGUARD_DEVICE_RULE="$malicious_rule" \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
|
||||||
|
token=$(basename "${request%.json}")
|
||||||
|
GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-always-output"
|
||||||
|
grep -Fqx "usbguard <allow-device> <--permanent> <$malicious_rule>" "$calls" || fail "review can persist an exact-device rule"
|
||||||
|
pass "USB review supports explicit persistent trust"
|
||||||
|
|
||||||
|
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
|
||||||
|
USBGUARD_DEVICE_ID=17 \
|
||||||
|
USBGUARD_DEVICE_EVENT=Insert \
|
||||||
|
USBGUARD_DEVICE_TARGET=block \
|
||||||
|
USBGUARD_DEVICE_RULE="$malicious_rule" \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
|
||||||
|
token=$(basename "${request%.json}")
|
||||||
|
device_rule_file="$scratch/blocked-device-rule"
|
||||||
|
printf '%s\n' "$malicious_rule" >"$device_rule_file"
|
||||||
|
replacement_rule='block id 1d50:60c7 name "Replacement gadget" hash "replacement"'
|
||||||
|
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
|
||||||
|
if BLOCKED_DEVICE_RULE_FILE="$device_rule_file" \
|
||||||
|
GUM_REASSIGN_RULE="$replacement_rule" \
|
||||||
|
GUM_CHOICE='Always allow this device' \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-reassigned-output" 2>&1; then
|
||||||
|
fail "review rejects a USBGuard ID reassigned while the approval dialog is open"
|
||||||
|
fi
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] ||
|
||||||
|
fail "an ID reassigned during review cannot authorize a different device"
|
||||||
|
[[ ! -e $request ]] || fail "a reassigned-ID review request is discarded"
|
||||||
|
pass "USB review revalidates identity after the approval dialog"
|
||||||
|
|
||||||
|
USBGUARD_IPC_SIGNAL=Device.PresenceChanged \
|
||||||
|
USBGUARD_DEVICE_ID=17 \
|
||||||
|
USBGUARD_DEVICE_EVENT=Insert \
|
||||||
|
USBGUARD_DEVICE_TARGET=block \
|
||||||
|
USBGUARD_DEVICE_RULE="$malicious_rule" \
|
||||||
|
"$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
|
||||||
|
token=$(basename "${request%.json}")
|
||||||
|
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
|
||||||
|
if BLOCKED_DEVICE_PRESENT=0 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-stale-output" 2>&1; then
|
||||||
|
fail "review rejects a request after its device disappears"
|
||||||
|
fi
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] ||
|
||||||
|
fail "a reused USBGuard id cannot authorize a different device"
|
||||||
|
[[ ! -e $request ]] || fail "a stale review request is discarded"
|
||||||
|
pass "USB review binds approval to the device snapshot"
|
||||||
|
|
||||||
|
sysfs="$scratch/sys/bus/usb/devices"
|
||||||
|
mkdir -p "$sysfs/usb1" "$sysfs/1-2"
|
||||||
|
echo 0 >"$sysfs/usb1/authorized_default"
|
||||||
|
echo 0 >"$sysfs/1-2/authorized"
|
||||||
|
export TEST_SYSFS="$sysfs"
|
||||||
|
|
||||||
|
"$ROOT/bin/omarchy-remove-security-usb-authorization" --yes >"$scratch/remove-output"
|
||||||
|
|
||||||
|
[[ $(<"$sysfs/usb1/authorized_default") == 1 ]] || fail "removal restores root-hub default authorization"
|
||||||
|
[[ $(<"$sysfs/1-2/authorized") == 1 ]] || fail "removal reauthorizes devices left connected"
|
||||||
|
grep -Fqx 'systemctl <disable --now usbguard.service>' "$calls" || fail "removal stops a service Omarchy enabled"
|
||||||
|
grep -Fqx 'usbguard <remove-user> <tester>' "$calls" || fail "removal drops the user's USBGuard IPC access"
|
||||||
|
grep -Fqx 'sudo <omarchy-usb-authorization-restore-default>' "$calls" ||
|
||||||
|
fail "removal restores USB authorization only through the fixed-path root helper"
|
||||||
|
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <disable>' "$calls" ||
|
||||||
|
fail "removing USB authorization also removes early-boot denial"
|
||||||
|
! grep -Fqx 'pkg-drop <usbguard>' "$calls" || fail "disabling keeps the default USBGuard package installed"
|
||||||
|
[[ ! -e $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
|
||||||
|
fail "removal deletes the graphical-session watcher"
|
||||||
|
pass "USB authorization removal restores the original default-allow behavior"
|
||||||
Reference in new issue
Block a user