Merge pull request #11874 from acrogenesis/feature/usb-device-authorization

Require approval for new USB and Thunderbolt devices by default.

Reported-by: Jurre van Bergen me@jurrevanbergen.nl
This commit is contained in:
Erik Melton authored and GitHub committed 2026-10-09 21:16:12 +02:00
commit 5cb31317b4
57 files changed
+5775 -2

No files matched your search

+2
View File
@@ -7,5 +7,7 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh"
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
run_logged "$OMARCHY_INSTALL/config/docker.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/firewall.sh"
run_logged "$OMARCHY_INSTALL/config/thunderbolt-authorization.sh"
@@ -0,0 +1,10 @@
echo "Configure Thunderbolt device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
# The builder's accessories must not become the new owner's trust policy.
# Keep ordinary Bolt behavior until interactive owner setup has finished.
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin prepare
systemctl enable omarchy-thunderbolt-authorization.service
fi
+26
View File
@@ -0,0 +1,26 @@
echo "Configure USB device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# 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}"
source "$OMARCHY_INSTALL/helpers/usb-authorization.sh"
usb_authorization_require_secure_settings "$daemon_config"
if [[ ! -s $rules_file ]]; then
usb_authorization_install_policy "$rules_file"
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.
systemctl enable usbguard.service
+319
View File
@@ -0,0 +1,319 @@
# Shared by the root controller and session commands. These paths are fixed at
# privileged entrypoints; tests may source the functions in an isolated fixture.
TB_STATE=/var/lib/omarchy/thunderbolt-authorization/policy.json
TB_CONFIG=/var/lib/boltd/boltd.conf
TB_MARKER=/etc/omarchy/thunderbolt-authorization.enabled
TB_PENDING=/etc/omarchy/thunderbolt-authorization.pending
TB_RUNTIME=/run/omarchy-thunderbolt-authorization
TB_LOCK=/run/lock/omarchy-thunderbolt-authorization.lock
TB_SERVICE=omarchy-thunderbolt-authorization.service
TB_BOLT=org.freedesktop.bolt
TB_MANAGER=org.freedesktop.bolt1.Manager
TB_DEVICE=org.freedesktop.bolt1.Device
TB_DOMAIN=org.freedesktop.bolt1.Domain
TB_PATH=/org/freedesktop/bolt
TB_SYSFS=/sys/bus/thunderbolt/devices
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
tb_json_write() {
local target=$1 mode=${2:-600} temporary
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
if jq -cS . > "$temporary" && chmod "$mode" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$target" && sync "${target%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_state() {
jq -ce 'select(.version == 1 and (.trusted | type == "object") and (.enabled | type == "boolean"))' "$TB_STATE"
}
tb_bus() {
busctl --system --auto-start=no --timeout=10 --json=short "$@"
}
tb_owner() {
tb_bus call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$TB_BOLT" | jq -er '.data[0]'
}
tb_properties() {
tb_bus call "$1" "$2" org.freedesktop.DBus.Properties GetAll s "$3" | jq -ce '.data[0] | map_values(.data)'
}
tb_set() {
local owner=$1 path=$2 interface=$3 key=$4 value=$5 signature=${6:-s} current
local -a values=()
current=$(tb_properties "$owner" "$path" "$interface") || return 1
if ! jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null; then
if [[ $signature == "as" ]]; then
mapfile -t values < <(jq -r '.[]' <<< "$value")
tb_bus set-property "$owner" "$path" "$interface" "$key" as "${#values[@]}" "${values[@]}" >/dev/null || return 1
else
tb_bus set-property "$owner" "$path" "$interface" "$key" s "$(jq -r . <<< "$value")" >/dev/null || return 1
fi
fi
# Bolt rejects no-op BootACL writes; even skipped writes are read back.
current=$(tb_properties "$owner" "$path" "$interface") || return 1
jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null ||
tb_fail "Bolt did not retain the requested $key"
}
tb_inventory() {
local owner manager paths path item inode domains='[]' devices='[]' stored='[]'
owner=$(tb_owner) || return 1
manager=$(tb_properties "$owner" "$TB_PATH" "$TB_MANAGER") || return 1
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDomains | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DOMAIN" | jq -c --arg path "$path" '. + {path:$path}') || return 1
domains=$(jq -c --argjson item "$item" '. + [$item]' <<< "$domains") || return 1
done < <(jq -r '.[]' <<< "$paths")
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDevices | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DEVICE" | jq -c --arg path "$path" '. + {path:$path}') || return 1
[[ $(jq -r .Type <<< "$item") == "peripheral" ]] || continue
if jq -e .Stored <<< "$item" >/dev/null; then
stored=$(jq -c --argjson item "$item" '. + [$item]' <<< "$stored") || return 1
fi
[[ $(jq -r .Status <<< "$item") != "disconnected" ]] || continue
inode=$(stat -Lc '%i' -- "$(jq -r .SysfsPath <<< "$item")") || return 1
item=$(jq -c --arg inode "$inode" '. + {inode:$inode}' <<< "$item") || return 1
devices=$(jq -c --argjson item "$item" '. + [$item]' <<< "$devices") || return 1
done < <(jq -r '.[]' <<< "$paths")
[[ $(tb_owner) == "$owner" ]] || { tb_fail "Bolt restarted while reading its inventory"; return 1; }
jq -cn --arg owner "$owner" --argjson manager "$manager" --argjson domains "$domains" \
--argjson devices "$devices" --argjson stored "$stored" '
{owner:$owner,manager:$manager,domains:$domains,stored:$stored,devices:[$devices[] |
. + {Secure:((.Domain as $domain | any($domains[]; .Uid == $domain and .SecurityLevel == "secure"))
and (.AuthFlags | contains("nokey") | not))}]}'
}
tb_read_inventory() {
local inventory
inventory=$(tb_inventory) || return 1
jq -e '.manager.AuthMode == "disabled"' <<< "$inventory" >/dev/null || {
tb_fail "Bolt automatic authorization is enabled; protection is not active"
return 1
}
printf '%s\n' "$inventory"
}
tb_identity() {
jq -c --arg owner "$1" --arg generation "$2" \
'{owner:$owner,generation:$generation,path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime}'
}
tb_snapshot() {
local state inventory generation recovery=false
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
[[ -f ${TB_STATE%/*}/boot-recovery.json ]] && recovery=true
jq -c --argjson state "$state" --arg generation "$generation" --argjson recovery "$recovery" \
--arg error "${1:-}" --argjson time "$(date +%s)" "$TB_TRUST_JQ"'
.owner as $owner |
[.devices[] | . as $d | {
identity: ({owner:$owner,generation:$generation} + ($d | {path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime})),
trusted: ($d | trusted($state)),
status:.Status,stored:.Stored,policy:.Policy,flags:.AuthFlags
}] as $devices |
{available:true,time:$time,owner:$owner,generation:$generation,devices:$devices,error:$error,
boot_protection:($state.boot_protection // false),
domains:[.domains[] | {Uid,SecurityLevel,IOMMU,BootACL}],
warnings: ([
(if $recovery then "A firmware boot-access change needs recovery. Boot protection is not confirmed." else empty end),
(.domains[] | if .SecurityLevel == "none" then
"This controller allows PCIe connections in firmware. Select user authorization in firmware settings; Omarchy cannot block its devices before their drivers load."
else empty end),
(.domains[] | .SecurityLevel as $level | select(["none","user","secure","dponly","usbonly","nopcie"] | index($level) | not) |
"The controller authorization mode could not be verified."),
(.domains[] | select($state.boot_protection and (.SysfsPath == "" or
(.SecurityLevel != "user" and .SecurityLevel != "secure") or (.BootACL | length == 0) or any(.BootACL[]; . != ""))) |
"The firmware boot allowlist is not confirmed empty on every controller. Check Thunderbolt Boot before relying on boot protection."),
($devices[] | select((.status | startswith("authorized")) and (.trusted | not) and (.flags | contains("boot"))) |
"An untrusted device was authorized by firmware before Linux started. Disconnect it safely to apply approval on its next connection.")
] | unique)}' <<< "$inventory"
}
tb_current() {
local expected=$1 inventory owner generation device identity
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if jq -e --argjson expected "$expected" '. == $expected' <<< "$identity" >/dev/null; then
jq -cn --arg owner "$owner" --argjson device "$device" '{owner:$owner,device:$device}'
return 0
fi
done < <(jq -c '.devices[]' <<< "$inventory")
tb_fail "That Thunderbolt device was removed or changed. Open its latest notification."
}
tb_prepare_key() (
local identity=$1 current key key_fd
current=$(tb_current "$identity") || return 1
# Bolt creates keys for stored devices itself. For a new device, disabled
# AuthMode prevents enrollment before authorization. Seed the kernel key so
# Authorize provisions it and the subsequent enrollment imports it into Bolt.
jq -e '.device.Secure and (.device.Stored | not)' <<< "$current" >/dev/null || return 0
cd -- "$(jq -r .device.SysfsPath <<< "$current")" || return 1
[[ $(stat -Lc '%i' .) == "$(jq -r .device.inode <<< "$current")" &&
$(cat unique_id) == "$(jq -r .device.Uid <<< "$current")" ]] || {
tb_fail 'The Thunderbolt connection changed before key setup'; return 1;
}
# Pin the attribute in the original directory; do not follow a replacement
# connection at the same path, or truncate an existing key on a retry.
exec {key_fd}<>key || return 1
key=$(cat key) || return 1
if [[ -z $key ]]; then
[[ $(cat authorized) == "0" ]] || { tb_fail 'Reconnect the device before saving secure approval'; return 1; }
key=$(openssl rand -hex 32) || return 1
[[ $key =~ ^[0-9a-f]{64}$ ]] || return 1
tb_current "$identity" >/dev/null || return 1
printf '%s' "$key" >&"$key_fd" || return 1
[[ $(cat key) == "$key" ]] || { tb_fail 'Could not verify the Thunderbolt key'; return 1; }
else
[[ $key =~ ^[0-9a-fA-F]{64}$ ]] || { tb_fail 'The Thunderbolt key is invalid'; return 1; }
fi
)
tb_approve() {
local identity=$1 permanent=$2 initialize=${3:-false} current owner path status state
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail "Thunderbolt protection is disabled"; return 1; }
current=$(tb_current "$identity") || return 1
owner=$(jq -r .owner <<< "$current")
path=$(jq -r .device.path <<< "$current")
status=$(jq -r .device.Status <<< "$current")
case "$status" in
connected|auth-error)
if [[ $permanent == "true" ]]; then
if [[ $initialize == "true" ]]; then
tb_prepare_key "$identity" || return 1
else
jq -e '(.device.Secure | not) or (.device.Stored and (.device.Key | IN("have","new")))' <<< "$current" >/dev/null || {
tb_fail 'A secure key requires explicit device approval'; return 1;
}
fi
fi
current=$(tb_current "$identity") || return 1
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null || return 1 ;;
authorized|authorized-newkey|authorized-secure) ;;
*) tb_fail "Device authorization is still in progress; try again"; return 1 ;;
esac
current=$(tb_current "$identity") || return 1
jq -e '.device.Status | IN("authorized","authorized-newkey","authorized-secure")' <<< "$current" >/dev/null || {
tb_fail "Could not confirm device authorization; try again"; return 1;
}
if [[ $permanent == "true" ]]; then
if ! jq -e .device.Stored <<< "$current" >/dev/null; then
# Save enrollment intent before Bolt writes its record, so a lost reply
# can be retried without confusing our record with a pre-existing one.
state=$(jq --argjson device "$(jq -c .device <<< "$current")" \
'.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state") || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$(jq -r .device.Uid <<< "$current")" manual '' >/dev/null || return 1
fi
current=$(tb_current "$identity") || return 1
jq -e .device.Stored <<< "$current" >/dev/null || { tb_fail "Could not confirm saved device information; try again"; return 1; }
jq -e '(.device.Secure | not) or (.device.Key | IN("have","new"))' <<< "$current" >/dev/null || {
tb_fail 'Secure approval has no saved key. Safely disconnect, reconnect, and approve the device again.'; return 1;
}
jq --argjson device "$(jq -c .device <<< "$current")" '
if .enrolled[$device.Uid] == ($device | {Uid,Name,Vendor}) then
.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor,StoreTime})
else . end |
.trusted[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state" |
tb_json_write "$TB_STATE" || return 1
fi
tb_snapshot
}
tb_boot_apply() {
local saved=$1 inventory owner item uid policies acls value path
inventory=$(tb_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
jq -e --argjson saved "$saved" '
(($saved.policies | keys) - [.stored[].Uid] | length == 0) and
(($saved.acls | keys) - [.domains[] | select(.SysfsPath != "") | .Uid] | length == 0)' <<< "$inventory" >/dev/null || {
tb_fail "Reconnect the original controllers and restore missing Bolt records before recovery"; return 1;
}
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.policies[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DEVICE" Policy "$value" || return 1
done < <(jq -c '.stored[]' <<< "$inventory")
# Changing a device policy can update firmware ACLs, so restore ACLs last.
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.acls[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
[[ $(jq length <<< "$value") == "$(jq '.BootACL | length' <<< "$item")" ]] || {
tb_fail "Firmware boot allowlist capacity changed"; return 1;
}
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DOMAIN" BootACL "$value" as || return 1
done < <(jq -c '.domains[]' <<< "$inventory")
}
tb_reconcile() {
local inventory state owner generation device identity error='' saved
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
if jq -e .boot_protection <<< "$state" >/dev/null; then
saved=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map(select(.SysfsPath != "" and (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length>0))) |
map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
tb_boot_apply "$saved" || return 1
fi
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if ! tb_approve "$identity" true >/dev/null; then
error='A trusted Thunderbolt device could not be restored. Check the system service log.'
fi
done < <(jq -c --argjson state "$state" "$TB_TRUST_JQ"'.devices | sort_by(.SysfsPath | length)[] |
select((.Status == "connected" or .Status == "auth-error") and (.AuthFlags | contains("nopcie") | not)) |
# Never silently initialize a missing secure binding on a later connection.
select(trusted($state))' <<< "$inventory")
tb_snapshot "$error"
}
tb_daemon() {
local snapshot error
install -d -m 755 "$TB_RUNTIME"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
chmod 644 "$TB_RUNTIME/generation"
while [[ -f $TB_MARKER ]]; do
(
flock -x 9
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$snapshot"
else
cat "$TB_RUNTIME/error" >&2
error=$(< "$TB_RUNTIME/error")
jq -cn --arg error "$error" --argjson time "$(date +%s)" '{available:false,time:$time,error:$error}' |
tb_json_write "$TB_RUNTIME/snapshot.json" 644
fi
) 9> "$TB_LOCK"
sleep 2
done
}
tb_public_snapshot() {
local snapshot generation
snapshot=$(cat "$TB_RUNTIME/snapshot.json") || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
jq -e --argjson now "$(date +%s)" --arg generation "$generation" \
'.available and .generation == $generation and .time <= $now and $now - .time < 20' <<< "$snapshot" >/dev/null || {
tb_fail 'Thunderbolt policy is unavailable or could not be verified.'; return 1;
}
printf '%s\n' "$snapshot"
}
+254
View File
@@ -0,0 +1,254 @@
# Session-side notification and terminal dialog functions.
tb_requests() {
TB_REQUESTS="$HOME/.local/state/omarchy/thunderbolt-authorization/requests"
mkdir -p -m 700 "$TB_REQUESTS"
chmod 700 "${TB_REQUESTS%/*}" "$TB_REQUESTS"
}
tb_safe_text() {
jq -r 'gsub("[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]"; "?")'
}
tb_notify() {
local token=$1 retry=${2:-false} title='Thunderbolt accessory blocked' description
description='Click to review it. Keep it blocked unless you recognize the device.'
if [[ $retry == "true" ]]; then
title='Thunderbolt approval needs retry'
description='The last approval could not be confirmed. Click to retry it.'
fi
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical --glyph '󱐋' \
"$title" "$description" --exec omarchy-launch-floating-terminal-with-presentation \
omarchy-thunderbolt-authorization-review "$token"
}
tb_attention() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Thunderbolt protection needs attention' 'The approval or firmware policy could not be confirmed. Click for details.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization status
}
tb_setup_notice() {
omarchy-notification-wait 1 >/dev/null || return 1
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
'Finish Thunderbolt protection setup' 'A connected accessory needs secure enrollment. Protection is not enabled. Click to finish safely.' \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization
}
tb_token() {
local digest
digest=$(jq -cS --arg watch "$2" '{watch:$watch,identity:.}' <<< "$1" | sha256sum) || return 1
printf 'request-%s\n' "${digest%% *}"
}
tb_request_read() {
[[ -f $1 && ! -L $1 ]] || return 1
jq -ce 'select(.identity | type == "object")' "$1"
}
tb_deliver() {
local path=$1 request token
request=$(tb_request_read "$path") || return 1
if ! jq -e '.notified // false' <<< "$request" >/dev/null; then
token=${path##*/}
if tb_notify "${token%.json}" "$(jq -r '.approval != null' <<< "$request")"; then
jq '.notified=true' <<< "$request" | tb_json_write "$path"
else
return 1
fi
fi
}
tb_request_apply() {
# The watcher has no terminal. Polkit permits this one fixed helper for an
# active local administrator; it cannot dispatch general setup operations.
pkexec --disable-internal-agent /usr/bin/omarchy-thunderbolt-authorization-approve "$1" "$2"
}
tb_request_process() {
local path=$1 request identity permanent result error_file error
request=$(tb_request_read "$path") || return 1
if jq -e '.pending // false' <<< "$request" >/dev/null; then
identity=$(jq -c .identity <<< "$request")
permanent=$(jq -r '.approval == "Always allow this device"' <<< "$request")
error_file=$(mktemp "$TB_REQUESTS/.error.XXXXXX") || return 1
if result=$(tb_request_apply "$permanent" "$identity" 2> "$error_file") &&
jq -e --argjson identity "$identity" --argjson permanent "$permanent" \
'any(.devices[]; .identity==$identity and (.status | IN("authorized","authorized-newkey","authorized-secure")) and
($permanent == false or .trusted))' <<< "$result" >/dev/null; then
jq --argjson result "$result" '.pending=false | .result=$result | del(.error)' <<< "$request" |
tb_json_write "$path" || return 1
else
error=$(cat "$error_file")
[[ -n $error ]] || error='Could not confirm approval. Click the retry notification.'
jq --arg error "$error" '.pending=false | .notified=false | .error=$error' <<< "$request" |
tb_json_write "$path" || return 1
fi
rm -f -- "$error_file"
fi
tb_deliver "$path"
}
tb_scan() {
local watch=$1 snapshot path request device identity token existing
snapshot=$(tb_public_snapshot) || return 1
(
flock -x 9
for path in "$TB_REQUESTS"/request-*.json; do
[[ -e $path || -L $path ]] || continue
if ! request=$(tb_request_read "$path"); then
rm -f -- "$path"
continue
fi
identity=$(jq -c .identity <<< "$request")
device=$(jq -c --argjson identity "$identity" '.devices[] | select(.identity==$identity)' <<< "$snapshot") || exit 1
if [[ -z $device ]] || { ! jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null &&
! jq -e '.approval != null' <<< "$request" >/dev/null; }; then
rm -- "$path"
continue
fi
if [[ $(jq -r .watch <<< "$request") != "$watch" ]]; then
rm -- "$path"
# Resume an interrupted attempt only for the same exact connection.
jq -e '.approval != null and .result == null' <<< "$request" >/dev/null || continue
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
request=$(jq -c --arg watch "$watch" '.watch=$watch | .notified=false' <<< "$request") || exit 1
tb_json_write "$path" <<< "$request" || exit 1
fi
tb_request_process "$path" || true
done
snapshot=$(tb_public_snapshot) || exit 1
while IFS= read -r device; do
identity=$(jq -c .identity <<< "$device")
token=$(tb_token "$identity" "$watch") || exit 1
path="$TB_REQUESTS/$token.json"
if [[ ! -f $path ]]; then
jq -cn --argjson identity "$identity" --arg watch "$watch" \
'{identity:$identity,watch:$watch,notified:false}' | tb_json_write "$path" || exit 1
if ! tb_deliver "$path"; then
rm -f -- "$path"
fi
fi
done < <(jq -c '.devices[] | select((.trusted | not) and (.status | IN("connected","auth-error")) and
(.flags | contains("nopcie") | not))' <<< "$snapshot")
) 9> "$TB_REQUESTS/.lock" || return 1
printf '%s\n' "$snapshot"
}
tb_watch() {
local watch snapshot warning warned='' failures=0
tb_requests
watch=$(cat /proc/sys/kernel/random/uuid)
while true; do
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
sleep 2
continue
fi
if snapshot=$(tb_scan "$watch"); then
failures=0
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
if jq -e '(.warnings | length > 0) or .error != ""' <<< "$snapshot" >/dev/null; then
if [[ $warning != "$warned" ]] && tb_attention; then
warned=$warning
fi
else
warned=''
fi
else
failures=$((failures + 1))
if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then
warned=unavailable
fi
fi
sleep 2
done
}
tb_find_device() {
local snapshot
snapshot=$(tb_public_snapshot) || return 1
jq -ce --argjson identity "$1" '.devices[] | select(.identity==$identity)' <<< "$snapshot" ||
tb_fail 'That Thunderbolt device was removed or changed. Open its latest notification.'
}
tb_review() {
local token=$1 path request identity device choice latest index
[[ $token =~ ^request-[0-9a-f]{64}$ ]] || { tb_fail 'Invalid or expired Thunderbolt request'; return 1; }
tb_requests
path="$TB_REQUESTS/$token.json"
request=$(tb_request_read "$path") || return 1
identity=$(jq -c .identity <<< "$request")
device=$(tb_find_device "$identity") || return 1
choice=$(jq -r '.approval // empty' <<< "$request")
if [[ -z $choice ]]; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || { tb_fail 'This device is no longer blocked'; return 1; }
gum style --foreground 212 --bold 'Blocked Thunderbolt accessory'
printf '\nThese details come from the device and can be forged:\n'
for index in Name Vendor Uid; do
printf '%s: ' "$index"
jq --arg key "$index" '.[$key]' <<< "$identity" | tb_safe_text
done
printf '\nApproval gives this accessory access to its PCIe drivers.\n\n'
choice=$(gum choose 'Allow once' 'Always allow this device' 'Keep blocked') || return 0
fi
case "$choice" in
'Allow once'|'Always allow this device'|'Keep blocked') ;;
*) tb_fail 'Invalid approval choice'; return 1 ;;
esac
(
flock -x 9
latest=$(tb_request_read "$path") || exit 1
[[ $latest == "$request" ]] || { tb_fail 'This request changed. Open the latest notification.'; exit 1; }
device=$(tb_find_device "$identity") || exit 1
if [[ $choice == "Keep blocked" ]]; then
printf 'Thunderbolt accessory remains blocked.\n'
else
if ! jq -e '.approval != null' <<< "$request" >/dev/null; then
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || exit 1
fi
jq --arg choice "$choice" '.approval=$choice | .pending=true | del(.error,.result)' <<< "$request" |
tb_json_write "$path"
fi
) 9> "$TB_REQUESTS/.lock" || return 1
if [[ $choice != "Keep blocked" ]]; then
for ((index=0; index<60; index++)); do
latest=$(tb_request_read "$path") || return 1
if jq -e '.result != null' <<< "$latest" >/dev/null; then
rm -- "$path"
if [[ $choice == "Always allow this device" ]]; then
printf 'Thunderbolt accessory trusted.\n'
else
printf 'Thunderbolt accessory allowed for this connection.\n'
fi
return 0
elif jq -e '.error != null' <<< "$latest" >/dev/null; then
jq .error <<< "$latest" | tb_safe_text >&2
return 1
fi
sleep 1
done
tb_fail 'Approval is still pending. The watcher will retain and retry the request.'
fi
}
tb_status() {
local snapshot
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
printf 'Thunderbolt protection is deferred. Previous Bolt behavior remains active.\nKeep a keyboard/input device available independently of Thunderbolt. Safely disconnect the Thunderbolt accessories, then run Setup > Security > Thunderbolt. Reconnect each accessory and choose Always allow to save its secure key.\n'
return 0
fi
snapshot=$(tb_public_snapshot) || return 1
printf 'Thunderbolt device approval is enabled.\nTrusted accessories reconnect automatically; new accessories require approval.\n'
if jq -e '.domains | length == 0' <<< "$snapshot" >/dev/null; then
printf 'No Thunderbolt controller is currently visible. Its firmware policy will be checked when it appears.\n'
fi
jq '.warnings[], (.error | select(. != ""))' <<< "$snapshot" | while IFS= read -r warning; do
printf 'Attention: '
tb_safe_text <<< "$warning"
done
printf 'Firmware boot access is separate; see the Security manual before relying on pre-boot protection.\n'
}
+398
View File
@@ -0,0 +1,398 @@
# Root setup functions. The caller sources thunderbolt-policy.sh first.
tb_config_authmode() {
local path=$1
if [[ -f $path ]]; then
awk '
/^[[:space:]]*\[/ { section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/) }
section && /^[[:space:]]*AuthMode[[:space:]]*=/ {
sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); mode=$0
}
END { print mode == "" ? "enabled" : mode }
' "$path"
else
printf 'enabled\n'
fi
}
tb_config_write() {
local path=$1 mode=$2 temporary
[[ $mode == "enabled" || $mode == "disabled" ]] || return 1
install -d -m 700 "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.config.XXXXXX") || return 1
if awk -v mode="$mode" '
/^[[:space:]]*\[/ {
section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/)
if (section) { found=1; print; print "AuthMode = " mode; next }
}
section && /^[[:space:]]*AuthMode[[:space:]]*=/ { next }
{ print }
END { if (!found) print "\n[config]\nAuthMode = " mode }
' "$(if [[ -e $path ]]; then printf '%s' "$path"; else printf /dev/null; fi)" > "$temporary" &&
sync "$temporary" && mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_guard() {
if [[ -f $TB_MARKER ]]; then
tb_state | jq -e .enabled >/dev/null || { tb_fail 'Thunderbolt policy and enable marker disagree'; return 1; }
tb_config_write "$TB_CONFIG" disabled
fi
}
tb_capture() {
local path uid name vendor trusted='{}'
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 ]] || continue
uid=$(cat "$path/unique_id") || return 1
# Bolt uses the numeric attribute when a descriptive name is absent.
if [[ -e $path/device_name ]]; then
name=$(cat "$path/device_name") || return 1
else
name=$(cat "$path/device") || return 1
fi
if [[ -e $path/vendor_name ]]; then
vendor=$(cat "$path/vendor_name") || return 1
else
vendor=$(cat "$path/vendor") || return 1
fi
[[ -n $uid ]] || { tb_fail 'A connected Thunderbolt device has no identity'; return 1; }
trusted=$(jq -c --arg uid "$uid" --arg name "$name" --arg vendor "$vendor" \
'.[$uid] = {Uid:$uid,Name:$name,Vendor:$vendor}' <<< "$trusted") || return 1
done
printf '%s\n' "$trusted"
}
tb_capture_ready() {
local fresh=${1:-false} offline=${2:-false} path uid inode inventory=''
# Do not relax an active policy, or capture new devices when re-enabling an
# existing policy. This preflight only guards the initial trust inventory.
[[ ! -f $TB_MARKER ]] || return 0
if [[ -f $TB_STATE && ! -f $TB_PENDING && $fresh != "true" ]]; then
tb_state | jq -e '.enrollment_pending // false' >/dev/null || return 0
fi
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 && -e $path/key ]] || continue
uid=$(cat "$path/unique_id") || return 1
[[ $uid =~ ^[0-9a-fA-F-]{36}$ ]] || { tb_fail 'A connected Thunderbolt device has an invalid identity'; return 1; }
path=$(realpath -e -- "$path") || return 1
inode=$(stat -Lc '%i' -- "$path") || return 1
# A key file alone can be orphaned after a failed Bolt record write. Ask
# Bolt whether this exact connection has a stored record and key. Offline
# prepare must not accept the live installer's daemon as the target store.
if [[ $offline != "true" && -z $inventory ]]; then
inventory=$(tb_inventory) || inventory='{}'
fi
if [[ $offline == "true" ]] || ! jq -e --arg uid "$uid" --arg path "$path" --arg inode "$inode" \
'any(.devices[]?; .Uid==$uid and .SysfsPath==$path and .inode==$inode and .Stored and (.Key | IN("have","new")))' <<< "$inventory" >/dev/null; then
# An active keyless dock cannot receive a key without disconnecting it.
# Preserve input/storage and finish installation with setup deferred.
install -d -m 755 "${TB_PENDING%/*}" || return 1
install -m 644 /dev/null "$TB_PENDING" || return 1
printf 'Thunderbolt protection is deferred: safely disconnect accessories without saved secure keys, keep independent input available, then run Setup > Security > Thunderbolt.\n' >&2
return 2
fi
done
}
tb_prepare() {
local fresh=${1:-false} state='{}' trusted mode
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
fi
if [[ ! -f $TB_STATE || -f $TB_PENDING || $fresh == "true" ]] || jq -e '.enrollment_pending // false' <<< "$state" >/dev/null; then
trusted=$(tb_capture) || return 1
mode=$(jq -r '.original_authmode // empty' <<< "$state") || return 1
[[ -n $mode ]] || mode=$(tb_config_authmode "$TB_CONFIG") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c --arg mode "$mode" --argjson trusted "$trusted" \
'del(.enrollment_pending) | . + {version:1,enabled:true,trusted:$trusted,original_authmode:$mode}' <<< "$state") || return 1
else
mode=$(jq -r .original_authmode <<< "$state") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c '.enabled=true' <<< "$state") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_config_write "$TB_CONFIG" disabled || return 1
install -d -m 755 "${TB_MARKER%/*}" || return 1
install -m 644 /dev/null "$TB_MARKER"
}
tb_boot_saved() {
jq -c '{policies:(.stored | map({key:.Uid,value:.Policy}) | from_entries),
acls:(.domains | map({key:.Uid,value:.BootACL}) | from_entries)}'
}
tb_boot_change() {
local enabled=$1 state inventory saved after desired backup="${TB_STATE%/*}/boot-recovery.json"
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail 'Enable Thunderbolt device authorization first'; return 1; }
if [[ $enabled == "false" ]] && ! jq -e .boot_protection <<< "$state" >/dev/null; then
return 0
fi
[[ ! -e $backup ]] || { tb_fail 'Recover the previous firmware boot-access change before retrying'; return 1; }
inventory=$(tb_read_inventory) || return 1
jq -e '.domains | length > 0 and all(.[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Connect every Thunderbolt controller before changing firmware boot access'; return 1;
}
saved=$(tb_boot_saved <<< "$inventory" | jq -c --argjson state "$state" '. + {state:$state}') || return 1
if [[ $enabled == "true" ]]; then
jq -e 'all(.domains[]; (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length > 0))' <<< "$inventory" >/dev/null || {
tb_fail 'This controller has no supported firmware boot allowlist. Disable Thunderbolt pre-boot/PCIe boot support in firmware settings; Omarchy cannot verify that setting.'; return 1;
}
desired=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
after=$(jq -c --argjson saved "$saved" --argjson devices "$(jq -c .devices <<< "$inventory")" '
if .boot_protection then . else .boot_original=($saved | {policies,acls}) end |
.boot_protection=true | .trusted += ($devices | map({key:.Uid,value:{Uid,Name,Vendor}}) | from_entries)' <<< "$state") || return 1
else
desired=$(jq -ce .boot_original <<< "$state") || return 1
after=$(jq -c 'del(.boot_original) | .boot_protection=false' <<< "$state") || return 1
fi
tb_json_write "$backup" <<< "$saved" || return 1
if tb_boot_apply "$desired" && tb_json_write "$TB_STATE" <<< "$after"; then
rm -- "$backup"
else
if tb_boot_apply "$saved" && tb_json_write "$TB_STATE" <<< "$state"; then
rm -- "$backup" || return 1
tb_fail 'Firmware change failed; previous state restored'
else
tb_fail "Firmware change failed; recovery is incomplete. Recovery data retained at $backup"
fi
return 1
fi
}
tb_boot_recover() {
local backup="${TB_STATE%/*}/boot-recovery.json" saved
saved=$(jq -ce . "$backup") || return 1
tb_boot_apply "$saved" || return 1
jq .state <<< "$saved" | tb_json_write "$TB_STATE" || return 1
rm -- "$backup"
}
tb_unit_state() {
local properties
properties=$(systemctl show -p ActiveState -p UnitFileState "$1") || return 1
jq -Rn '[inputs | split("=") | {key:.[0],value:.[1]}] | from_entries |
{active:(.ActiveState == "active"),enabled:(.UnitFileState == "enabled")}' <<< "$properties"
}
tb_handoff_policies() {
local state=$1
# StoreTime distinguishes a record recreated outside Omarchy. A completed
# enrollment and matching consent are both required, including offline
# accessories. Never promote somebody else's manual policy.
jq -c --argjson state "$state" '{policies:([.stored[] |
select(.Policy == "manual" and .StoreTime > 0 and
$state.enrolled[.Uid] == {Uid,Name,Vendor,StoreTime} and
$state.trusted[.Uid] == {Uid,Name,Vendor}) |
{key:.Uid,value:"auto"}] | from_entries),acls:{}}'
}
tb_config_checkpoint() {
local backup="${TB_STATE%/*}/setup-recovery.json" path original files='{}' units='{}' unit value saved inventory data mode state
[[ ! -e $backup && ! -e ${TB_STATE%/*}/boot-recovery.json ]] || {
tb_fail 'A previous setup change needs recovery before continuing'; return 1;
}
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=null
if [[ -e $path ]]; then
data=$(base64 -w0 -- "$path") || return 1
mode=$(stat -c '%a' -- "$path") || return 1
original=$(jq -cn --arg data "$data" --arg mode "$mode" '{data:$data,mode:$mode}') || return 1
fi
files=$(jq -c --arg path "$path" --argjson original "$original" '.[$path]=$original' <<< "$files") || return 1
done
for unit in bolt.service "$TB_SERVICE"; do
value=$(tb_unit_state "$unit") || return 1
units=$(jq -c --arg unit "$unit" --argjson value "$value" '.[$unit]=$value' <<< "$units") || return 1
done
saved=$(jq -cn --argjson files "$files" --argjson units "$units" '{files:$files,units:$units}') || return 1
state='{}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
# A handoff to auto also changes Bolt's firmware allowlists. Checkpoint the
# policies and ACLs even when the optional boot protection was never used.
if jq -e '.enabled and (.boot_protection or ((.enrolled // {}) | length > 0))' <<< "$state" >/dev/null; then
inventory=$(tb_read_inventory) || return 1
jq -e 'all(.domains[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Reconnect every Thunderbolt controller before changing device policies'; return 1;
}
saved=$(jq -c --argjson firmware "$(tb_boot_saved <<< "$inventory")" '.firmware=$firmware' <<< "$saved") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$backup" <<< "$saved"
}
tb_config_restore() {
local backup="${TB_STATE%/*}/setup-recovery.json" saved path original temporary unit
saved=$(jq -ce . "$backup") || return 1
systemctl stop "$TB_SERVICE" bolt.service || return 1
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=$(jq -c --arg path "$path" '.files[$path]' <<< "$saved") || return 1
if [[ $original == "null" ]]; then
rm -f -- "$path" || return 1
else
mkdir -p -- "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.restore.XXXXXX") || return 1
if jq -r .data <<< "$original" | base64 -d > "$temporary" &&
chmod "$(jq -r .mode <<< "$original")" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
:
else
rm -f -- "$temporary"
return 1
fi
fi
done
if jq -e .firmware <<< "$saved" >/dev/null; then
systemctl start bolt.service || return 1
tb_boot_apply "$(jq -c .firmware <<< "$saved")" || return 1
fi
rm -f -- "${TB_STATE%/*}/boot-recovery.json" || return 1
if jq -e --arg unit "$TB_SERVICE" '.units[$unit].enabled' <<< "$saved" >/dev/null; then
systemctl enable "$TB_SERVICE" || return 1
else
systemctl disable "$TB_SERVICE" || return 1
fi
for unit in bolt.service "$TB_SERVICE"; do
if jq -e --arg unit "$unit" '.units[$unit].active' <<< "$saved" >/dev/null; then
systemctl start "$unit" || return 1
else
systemctl stop "$unit" || return 1
fi
done
rm -- "$backup"
}
tb_transaction() {
tb_config_checkpoint || return 1
if "$@"; then
rm -- "${TB_STATE%/*}/setup-recovery.json"
else
if tb_config_restore; then
tb_fail 'Thunderbolt setup failed; previous configuration restored'
else
tb_fail 'Thunderbolt setup failed; restoration is incomplete. Run the setup recovery command.'
fi
return 1
fi
}
tb_enable() {
systemctl stop "$TB_SERVICE" bolt.service || return 1
tb_prepare "${1:-false}" || return 1
systemctl daemon-reload || return 1
systemctl start bolt.service || return 1
tb_read_inventory >/dev/null || return 1
systemctl enable --now "$TB_SERVICE"
}
tb_disable() {
local state mode policies
systemctl stop "$TB_SERVICE" || return 1
tb_boot_change false || return 1
state=$(tb_state) || return 1
mode=$(jq -er .original_authmode <<< "$state") || return 1
policies=$(tb_read_inventory | tb_handoff_policies "$state") || return 1
tb_boot_apply "$policies" || return 1
systemctl stop bolt.service || return 1
tb_config_write "$TB_CONFIG" "$mode" || return 1
rm -f -- "$TB_MARKER" || return 1
jq '.enabled=false | del(.enrolled)' <<< "$state" | tb_json_write "$TB_STATE" || return 1
systemctl disable "$TB_SERVICE" || return 1
systemctl start bolt.service || return 1
tb_inventory | jq -e --arg mode "$mode" --argjson policies "$policies" '
.manager.AuthMode==$mode and
(([.stored[] | {key:.Uid,value:.Policy}] | from_entries) as $actual |
all($policies.policies | to_entries[]; $actual[.key]==.value))' >/dev/null
}
tb_cancel_pending() {
local state
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
else
state=$(jq -cn --arg mode "$(tb_config_authmode "$TB_CONFIG")" \
'{version:1,enabled:false,trusted:{},enrollment_pending:true,original_authmode:$mode}') || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
jq '.enabled=false | .enrollment_pending=true' <<< "$state" | tb_json_write "$TB_STATE" || return 1
rm -- "$TB_PENDING"
}
tb_reset_root() {
local root directory
root=$(realpath -e -- "$1") || return 1
[[ $root != "/" && -d $root ]] || { tb_fail 'Refusing to reset the running system'; return 1; }
[[ -e $root$TB_MARKER || -e $root$TB_PENDING || -d $root${TB_STATE%/*} ]] || return 0
# Resolve fixed paths inside the cloned root, never through an outside link.
for directory in etc etc/omarchy var var/lib var/lib/omarchy var/lib/boltd; do
[[ ! -L $root/$directory ]] || { tb_fail 'Refusing a linked factory policy directory'; return 1; }
done
systemctl --root="$root" disable "$TB_SERVICE" || return 1
rm -f -- "$root$TB_MARKER" || return 1
rm -f -- "$root$TB_PENDING" || return 1
rm -rf -- "$root${TB_STATE%/*}" || return 1
for directory in devices keys domains; do
rm -rf -- "$root/var/lib/boltd/$directory" || return 1
done
tb_config_write "$root/var/lib/boltd/boltd.conf" enabled
}
tb_admin() {
local action=${1:-} status=0 state
if [[ $action != "guard" ]]; then
exec 9> "$TB_LOCK"
flock -x 9 || return 1
fi
case "$action" in
prepare|enable|owner|migrate)
# A deferred setup needs explicit consent after the accessories have
# been disconnected; a later migration must not silently finish it.
if [[ -f $TB_PENDING && $action != "enable" ]]; then return 0; fi
if [[ $action == "migrate" && -f $TB_STATE ]] && tb_state | jq -e '.enabled == false' >/dev/null; then return 0; fi
if tb_capture_ready "$(if [[ $action == "owner" ]]; then echo true; else echo false; fi)" \
"$(if [[ $action == "prepare" ]]; then echo true; else echo false; fi)"; then
:
else
status=$?
if (( status == 2 )); then return 0; else return "$status"; fi
fi ;;
esac
case "$action" in
guard) tb_guard ;;
prepare) tb_prepare && rm -f -- "$TB_PENDING" ;;
enable) tb_transaction tb_enable && rm -f -- "$TB_PENDING" ;;
owner) tb_transaction tb_enable true && rm -f -- "$TB_PENDING" ;;
migrate)
state='{"enabled":true}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
if jq -e .enabled <<< "$state" >/dev/null; then
tb_transaction tb_enable
fi ;;
disable)
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
tb_cancel_pending
else
tb_transaction tb_disable
fi ;;
recover) tb_config_restore ;;
boot-enable|boot-disable|boot-recover)
systemctl stop "$TB_SERVICE" || return 1
case "$action" in
boot-enable) tb_boot_change true || status=$? ;;
boot-disable) tb_boot_change false || status=$? ;;
boot-recover) tb_boot_recover || status=$? ;;
esac
systemctl start "$TB_SERVICE" || return 1
return "$status" ;;
reset-root) [[ $# == 2 ]] && tb_reset_root "$2" ;;
*) tb_fail 'Unknown Thunderbolt setup action' ;;
esac
}
@@ -0,0 +1,95 @@
# Device identity is portable; the full live rule is still used when granting
# access to the connection shown in the approval dialog.
usb_authorization_portable_rule() {
local rule="$1" token="" char quoted=0 escaped=0 index
local -a tokens=() portable=()
# Split canonical USBGuard output without evaluating device-controlled text.
# An attribute name inside a quoted name/serial is never a rule attribute.
for (( index=0; index<${#rule}; index++ )); do
char=${rule:index:1}
if (( escaped )); then
token+="$char"
escaped=0
elif (( quoted )) && [[ $char == '\' ]]; then
token+="$char"
escaped=1
elif [[ $char == '"' ]]; then
token+="$char"
quoted=$((1 - quoted))
elif (( ! quoted )) && [[ $char == " " ]]; then
if [[ -n $token ]]; then tokens+=("$token"); token=""; fi
else
token+="$char"
fi
done
(( ! quoted && ! escaped )) || return 1
if [[ -n $token ]]; then tokens+=("$token"); fi
[[ ${tokens[0]:-} == "allow" || ${tokens[0]:-} == "block" ]] || return 1
portable=(allow)
for (( index=1; index<${#tokens[@]}; index++ )); do
case "${tokens[index]}" in
via-port | parent-hash)
(( ++index < ${#tokens[@]} )) || return 1
[[ ${tokens[index]} == \"*\" ]] || return 1
;;
label) return 1 ;;
*) portable+=("${tokens[index]}") ;;
esac
done
local IFS=" "
printf '%s label "omarchy-usb-authorization-v1"\n' "${portable[*]}"
}
usb_authorization_permanent() {
local id="$1" expected="$2" devices current="" allowed portable policy line target new_id matches rule saved=0
[[ $id =~ ^[0-9]+$ && ( $expected == block\ * || $expected == allow\ * ) ]] || return 1
(( ${#expected} <= 16384 )) || return 1
devices=$(usbguard list-devices) || return 1
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then current=${line#"$id: "}; fi
done <<<"$devices"
[[ $current == "$expected" ]] || return 1
allowed="allow ${expected#* }"
portable=$(usb_authorization_portable_rule "$current") || return 1
usbguard-rule-parser "$portable" >/dev/null || return 1
# Respect earlier administrator deny rules. Do not report permanent success
# when a saved allow would lose to an explicit block/reject on reconnect.
policy=$(usbguard list-rules) || return 1
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]](allow|block|reject)([[:space:]]|$) ]]; then
target=${BASH_REMATCH[1]}
rule=${line#*: }
# --show-devices filters by current target, hiding deny rules for a
# temporarily allowed device. A match query compares identity instead.
matches=$(usbguard list-devices "match${rule#"$target"}") || return 1
if grep -Fqx -- "$id: $current" <<<"$matches"; then
if [[ $target != "allow" ]]; then
echo "An administrator rule blocks this device. Review that rule before saving approval." >&2
return 1
fi
break
fi
fi
done <<<"$policy"
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]] && ${line#*: } == "$portable" ]]; then
saved=1
fi
done <<<"$policy"
# A retry can reuse only an exact owned rule present in both the daemon and
# its persistent file. Never remove/reorder rules around manual restrictions.
if (( ! saved )) || ! grep -Fqx -- "$portable" /etc/usbguard/rules.conf; then
new_id=$(usbguard append-rule "$portable") || return 1
[[ $new_id =~ ^[0-9]+$ ]] || return 1
grep -Fqx -- "$portable" /etc/usbguard/rules.conf || return 1
fi
if [[ $current == block\ * ]]; then
usbguard allow-device "$expected" || return 1
fi
devices=$(usbguard list-devices --allowed) || return 1
grep -Fqx -- "$1: $allowed" <<<"$devices"
}
+146
View File
@@ -0,0 +1,146 @@
source "${BASH_SOURCE[0]%/*}/usb-authorization-policy.sh"
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 || 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_require_secure_setting "$config" RuleFile /etc/usbguard/rules.conf || return 1
}
usb_authorization_generate_policy() {
local policy="$1" generated line
if ! generated=$(usbguard generate-policy); then
echo "USBGuard could not enumerate devices; nothing was enabled." >&2
return 1
fi
: >"$policy"
while IFS= read -r line; do
if [[ $line == allow\ * ]]; then
usb_authorization_portable_rule "$line" >>"$policy" || return 1
elif [[ -n $line ]]; then
printf '%s\n' "$line" >>"$policy"
fi
done <<<"$generated"
if ! grep -q '^allow ' "$policy"; then
if grep -q '^[[:space:]]*[^#[:space:]]' "$policy"; then
echo "USBGuard did not generate a usable policy; nothing was enabled." >&2
return 1
fi
# A successful empty inventory is valid on machines without USB controllers.
# Keep the file nonempty so re-enabling preserves this default-deny policy.
echo '# No USB devices were present during enrollment.' >>"$policy"
fi
usbguard-rule-parser -f "$policy" >/dev/null
}
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_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 devices
local count=0
devices=$(usbguard list-devices) || return 1
local -a inventory
mapfile -t inventory <<<"$devices"
for line in "${inventory[@]}"; do
if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then
id="${BASH_REMATCH[1]}"
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "${line#*: }" || return 1
(( ++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
}
+1
View File
@@ -144,6 +144,7 @@ udiskie
ufw
ufw-docker
unzip
usbguard
usage
uwsm
vi
+3 -1
View File
@@ -19,6 +19,8 @@ systemctl --user enable --now \
omarchy-sleep-lock.service \
omarchy-migrate-notify.service \
omarchy-fcitx5.service \
omarchy-crash-watch.service
omarchy-crash-watch.service \
omarchy-usb-authorization.service \
omarchy-thunderbolt-authorization.service
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync