Skip to content

Require approval for new USB and Thunderbolt devices by default - #11874

Merged
ErikMelton merged 17 commits into
omacom:quattrofrom
acrogenesis:feature/usb-device-authorization
Oct 9, 2026
Merged

ErikMelton merged 17 commits into
omacom:quattrofrom
acrogenesis:feature/usb-device-authorization

Conversation

@acrogenesis

@acrogenesis acrogenesis commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

This PR enables approval by default for new USB and Thunderbolt/USB4 PCIe accessories, using USBGuard and Bolt respectively.

Unknown USB accessories can reach matching kernel drivers as soon as they are connected. A malicious gadget can spoof a supported device identity and reach a vulnerable driver, as in the public Pegasus USB Ethernet report.

This enables USBGuard authorization by default for fresh installs and existing installations through a migration. Devices connected during enrollment are trusted. Deferred installs leave USBGuard disabled until owner provisioning finishes, then enroll the owner’s connected hardware before enabling enforcement. Factory reset also disables inherited USBGuard enforcement in the staged root before handing it to the new owner. New devices remain blocked until the user chooses Allow once, Always allow this device, or Keep blocked from a persistent notification. Remove > Security > USB Device Authorization disables protection; re-enabling preserves the saved policy.

Authorization and notification behavior

  • Device descriptors remain data in private JSON requests. Only an opaque token crosses the terminal launcher. Approval rechecks the exact device after the dialog and targets its complete USBGuard rule, preventing a recycled numeric ID from authorizing a replacement device. It verifies the resulting allowed state and, for permanent approval, a matching policy rule before reporting success or consuming the request. The request saves the selected approval before authorization. Failed verification retains that intent; retry checks the same device across blocked and allowed states, resumes verification if already allowed, and still requires saved policy for permanent approval. Transient inventory failures preserve the request. Request updates share the watcher lock, and a dialog whose request changed cannot authorize it.
  • Notifications use the final policy result. Trusted reconnects do not produce false blocked warnings. Every daemon connection scans blocked devices, restoring prompts when a restart revokes temporary approval. Deduplication is scoped to the daemon invocation and watcher lifetime, so an old request cannot suppress a fresh notification after restart. Removal cleans up requests while protecting a reappeared device from delayed events.
  • Delivery waits for the desktop notification service and retries failed sends automatically while rechecking the blocked device. One failed request does not abort the remaining scan; concurrent events deduplicate pending requests.
  • Successful enrollment with no USB hardware stores an initialized comment-only policy. Enumeration failures still abort setup, and re-enabling does not silently enroll newly attached devices.

Portable USB approvals and watcher lifecycle

Permanent USB approvals, initial enrollment, and the optional boot trust snapshot retain the device identity while omitting via-port and parent-hash. Moving the same device between ports or hubs no longer needs another approval. The approval dialog still verifies the full live connection; a narrow protected-Bash Polkit helper saves a labelled identity rule, verifies persistence and authorization, and preserves existing manual rule order. Changes to the device identity still require approval.

Older unmarked rules cannot reliably be distinguished from manual restrictions. omarchy-setup-security-usb-authorization --review-existing asks individually which connected devices should receive portable trust, preserving existing rules. Disconnected devices can be reapproved when connected. Fresh approvals carry an Omarchy label.

Setup validates the session/runtime and installed helpers, replaces dangling watcher links with a copied unit, and starts the watcher before enabling USBGuard. Removal confirms USBGuard stopped before removing the watcher or its access. The repair migration respects an existing opt-out.

Validation for c3cdcb1e: 30 USB behavior groups, 16 parser/startup/native-policy checks (including 13 checks against libusbguard), 112 CLI checks, and the focused boot-signing, enrollment, factory-reset and first-run suites passed. Real Lab testing repaired active USBGuard with a missing watcher, exercised graphical permanent approval through Polkit, moved an approved mouse between root ports and between two hubs, and verified authorization after daemon restart and a full guest reboot. A different device stayed blocked and received a request. Legacy review accepted/declined individual devices without rewriting the original rules; retries left verified policy unchanged. QEMU changes its descriptor hash when switching USB speeds, which correctly required fresh approval. Physical Logitech hardware was not used. Temporary virtual devices were removed and the guest's original policy restored after testing.

Optional boot protection

The default policy starts with USBGuard. Setup > Security > USB at Boot additionally captures currently connected devices into the trusted policy and applies usbcore.authorized_default=0 to current and Limine snapshot boot entries. The helper verifies effective command lines, image hashes, and embedded UKI command lines. It updates snapshot manifests and recovery copies under Limine's shared lock so later synchronization preserves the selected policy. Removal verifies permissive boot entries before full protection can be disabled; boot-only removal keeps USBGuard running.

Under Secure Boot, conflicting historical UKIs are prepared before changing boot settings: verify the original hash and signature, re-sign a copy with the requested USB parameter, and update archived image names, menu hashes, and primary/backup/cache manifest references under Limine’s lock. Other embedded boot arguments and executable code are preserved. Signing failures stop the transition before settings change. Custom multi-profile or signed-PCR-policy images require their original builder; the command explains this before changing policy.

Secure Boot transitions checkpoint the boot files, snapshot cache, and USB boot settings before changes, while holding Limine’s shared lock. Nested rebuild/enrollment tools use a private mount namespace and lock inode. The helper independently verifies the configured EFI bootloader’s signature and enrolled menu hash after enrollment and before accepting the transition. If signing or verification fails, it restores the previous signed executable together with its matching menu, kernels, metadata, and settings without re-signing. This requires temporary free space on the system drive; an incomplete recovery retains its checkpoint and reports its location.

Boot protection requires disk unlock and recovery without any USB devices. Even trusted USB input is unavailable at an encrypted-disk prompt before USBGuard starts. Older snapshots predating USBGuard keep all USB disabled after startup, including keyboards, networking, and storage. The confirmation, command help, and manual disclose this limitation. Trusting current devices does not make them available in those old roots.

This is defense in depth. An approved device can still reach its driver, and device descriptors are forgeable. The Pegasus driver fix remains an upstream kernel responsibility.

Thunderbolt and USB4 PCIe authorization

Thunderbolt/USB4 PCIe accessories now use the same approval choices through Bolt. Bash handles policy, setup, notifications, and dialogs using structured busctl JSON and jq. A root service keeps Bolt automatic authorization disabled, restores explicitly trusted devices, and publishes a root-owned status snapshot. New accessories require approval even when IOMMU protection is available. Trust is separate from Bolt's automatic firmware imports.

The session watcher submits individual approvals through a fixed Polkit-scoped pkexec helper. Protected Bash startup, fixed installed libraries, argument validation, exact live-connection checks, and independent readback guard the privileged boundary. Requests bind both daemon generations and the connection identity; stale snapshots fail closed, failed notifications retry, and lost replies retain approval intent. Active local wheel users can approve individual accessories; blanket policy changes require authentication.

Thunderbolt setup trusts initially connected accessories, preserves trust on re-enable, respects opt-out during migration, defers enrollment until owner provisioning, and scrubs old trust, keys, recovery data, and pending setup from both factory-reset roots. Initial capture matches Bolt's numeric device/vendor fallback when descriptive sysfs names are absent. USB and Thunderbolt enrollment and session services are both retained.

On secure-capable hardware, permanent approval initializes a kernel key before authorization and verifies that Bolt saved it before recording trust. Reconnection must use that saved key; automatic restoration cannot create a replacement binding. The root daemon retains its read-only sysfs sandbox. If initial setup cannot confirm saved keys for connected secure accessories, it leaves the previous Bolt policy active and shows Finish Thunderbolt protection setup. This avoids stranding a keyboard or disconnecting active storage. Setup explains how to safely disconnect accessories with independent input, enable protection, then reconnect and approve each device. Offline preparation cannot use the installer's Bolt records as proof of target-system enrollment. Canceling deferred setup preserves opt-out without treating initial enrollment as complete.

Setup / Remove > Security > Thunderbolt controls runtime protection. Optional Thunderbolt Boot clears supported firmware allowlists, changes stored policies to manual, verifies readback, and retains recovery checkpoints. This requires online controllers with a supported BootACL in user or secure mode. Firmware mode none cannot provide a barrier before PCIe drivers load. Trusted Thunderbolt input or storage may remain unavailable until the policy service starts; other operating systems and older snapshots can change firmware permissions. USB functions on docks still use USBGuard separately.

Removing runtime protection now hands matching records enrolled by Omarchy to Bolt’s automatic policy before restoring the global authorization mode. Existing manual policies and secure keys are preserved, including when the approved accessory is disconnected. Enrollment ownership is recorded separately from trust, with retry intent and Bolt store timestamps. Setup checkpoints device policies and firmware allowlists along with configuration and services; removal verifies the policies after restarting Bolt and rolls back failures. Automatic policies resume Bolt’s normal firmware allowlist behavior, so the controllers must be online for a recoverable handoff.

Architecture and test instructions: Thunderbolt authorization.

Validation

Astra and Fable completed full reviews of the earlier combined implementation at a3340fdb. Erik subsequently found a removal regression: Always allow created a manual Bolt record that stopped reconnecting when Omarchy protection was removed. This is fixed in the single amended Thunderbolt commit 60d6b255; the existing USB history is retained. The removal fix has focused regression and real-Bolt validation below; it has not had a fresh independent agent review.

Combined-branch validation: USB authorization, USB archive/enrollment recovery, factory reset, owner provisioning, first-run, menu/guards, and the full CLI suite pass. Thunderbolt has 59 Bash behavior tests, three protected-startup checks, and five passing integration cases against real boltd 0.9.11 with UMockdev hardware. The integration cases cover user/secure modes and missing descriptive names; secure approval saves a key on the first connection and reconnect requests authentication with the same key. Policy tests and assertions are Bash; only the upstream hardware simulator remains Python. Bash syntax and diff checks pass.

Removal regression validation: the new behavior test fails against the previous helpers because the approved record stays manual. With this fix, real private boltd tests pass in user and secure modes with IOMMU enabled: Always allow → failed removal and rollback → successful removal → reconnect works automatically, while an independently enrolled manual device remains blocked. The secure case includes boot protection, removal while the accessory is disconnected, and verification that the original key is retained and used for secure authentication. The setup and approval functions and policy daemon are real; the systemd bridge manages only the fixture’s private processes. Native tests also cover lost enrollment replies, opt-out/re-enable, externally recreated records, policy readback failures, rollback recovery, and unavailable controllers.

Lab validation covers actual installed systemd/Polkit helpers, startup enforcement, deferred setup cancellation and explicit completion, opt-out and re-enable, malformed/stale approval rejection, and the new pending notification/setup dialog. Earlier Lab checks covered both approval choices, lost-reply recovery, and blocking while locked. Guest policy and files were restored after testing. Physical Thunderbolt/DMA, actual Thunderbolt firmware boot enforcement, power-loss recovery, and fresh/OEM ISO installation remain unverified. The USB firmware tests below apply specifically to USB.

Focused USB regressions and the CLI suite pass. Coverage includes enrollment and enumeration failures, policy preservation, notification readiness and retries across multiple devices, reconnect scans, identity changes during review, and boot-policy regeneration. Bash syntax and whitespace checks pass.

Live Omarchy Lab checks:

  • Trusted tablet reconnects produce no false warnings. A blocked mouse approved once receives a fresh prompt after a USBGuard restart.
  • Approval retries: a subprocess-only test shim failed one verification query after real USBGuard authorization, separately for Allow once and Always allow this device. Each retained request then completed successfully with only one authorization call; permanent retry verified the real saved policy. Expanded USB regressions pass locally and in Lab, including query failures, missing policy, replacement identities, and changed dialogs. Fixed in db6ab5a8. Lab policy was restored and test devices removed.
  • With the notification shell stopped, two blocked devices waited. Restoring the shell produced both prompts without another device event or watcher restart. Both visible alerts were inspected.
  • Actual USBGuard policy generation with an empty USB sysfs view in a private mount namespace succeeds with zero bytes; the helper persists the initialized empty policy.
  • Snapshot protection survives standalone synchronization. Disable followed by synchronization keeps both historical and newly created snapshot entries permissive.
  • The updated boot confirmation was inspected at 1280×800 and canceled without changing boot policy.

Historical UKI validation for de72b648: the new archive suite covers both transitions, cached/structured/nested references, unchanged current-image hashes, argument/code preservation, signing and enrollment failures, unsigned/corrupt originals, retries, and optional-tool lock ordering. Real sbctl/sbattach tests with ephemeral keys passed locally and in Lab, including root-only main-path success and preflight failure. In a private Lab mount namespace, the shipped helper then enabled and disabled protection using a copied real historical UKI and the real Limine rebuild, synchronization, and enrollment commands. A separate synchronization preserved each result. That initial namespace test validated signing and boot-file transitions; the subsequent firmware boot tests below cover actual enforcement. The original Lab guest’s USB policy was unchanged. Existing USB and CLI suites, syntax, and whitespace checks pass.

Bootloader signing recovery in e12c8ed4: a full Astra xhigh review found that Limine’s wrapper masks bootloader-signing failures. New regressions cover signing failure, stale enrollment, early rebuild failure, false-success hooks, recovery of existing/absent settings and manifests, and success with enrollment enabled or disabled. Real-signature tests pass locally and in Lab. A root Lab test exercises the actual namespace runner and installed Limine enrollment/restore functions. Full main-path tests using copied boot files then inject bootloader-only signing failure for both enable and disable: each rejects the transition and restores every preexisting boot-file hash and the matching signed bootloader/menu. Removing the fault lets both transitions and a separate synchronization succeed with real Limine tools. Those initial copied-filesystem tests did not exercise firmware or power-loss recovery; the subsequent firmware boot tests below cover enforcement and a reboot after signing-failure recovery. The original Lab policy and machine keys were unchanged. USB, archive, CLI, syntax, and whitespace checks pass. No fresh Astra rereview of this fix is claimed.

Earlier Lab boot/lock validation: insertion while locked remained unauthorized with no interface or block device. Normal boot exposed the expected gap before USBGuard starts. With boot protection enabled, an enrolled QEMU tablet remained unauthorized until USBGuard matched it, then appeared as a working HID mouse. An untrusted storage device stayed unauthorized. Enable/disable and policy preservation were exercised through the shipped commands. Booting into a legacy snapshot without USBGuard was not tested; its limitation was confirmed from the policy path and a safe fixture.

Earlier Astra review identified the legacy-snapshot disclosure gap, now covered in the confirmation/help/manual. Subsequent review found the deferred-owner enrollment and false-success approval issues addressed by this revision. A fresh full Astra xhigh review at afad8530 found one additional stale-request lifecycle defect. The fix in 90d604f4 passed a targeted Astra xhigh rereview with no actionable findings.

Latest review fixes: 67d173c9 defers enrollment and enforcement until owner provisioning finishes; 58a9c95b verifies device and policy state after approval. In Lab, a newly attached owner keyboard worked before enforcement, stayed allowed after enrollment and daemon restart, and a later untrusted mouse was blocked. With deliberately conflicting policy rules, real USBGuard returned success without allowing the mouse; the updated graphical dialog reported failure and retained the request. After removing the conflict, retrying that request succeeded, saved the policy, and survived a daemon restart. The full OEM ISO provisioning flow was not rerun; its setup leaf and owner-enrollment helper were exercised directly. Focused USB, CLI, provisioning groups/user, syntax, and whitespace checks pass. Lab’s original trust policy was restored and all temporary devices removed.

Earlier focused menu, config, systemd, provisioning, and unit-file verification checks also passed. The earlier aggregate shell run had unrelated baseline and host temporary-storage failures; it is not claimed as a clean full-suite pass.

Factory-reset follow-up: afad8530 disables the restored USBGuard service in the staged factory root, keeping a new owner's keyboard usable until enrollment. Failure to disable aborts staging before the root is activated. A regression test runs the actual staging functions with real offline systemctl in temporary roots, covering enabled/disabled/absent USBGuard, disable failure, and later re-enabling. It fails on the previous implementation and passes with the fix. This test and the USB suite pass on the host and in Lab; CLI, syntax, and whitespace checks also pass. No destructive full factory reset was performed.

Notification lifecycle validation for 90d604f4: in Lab, dismissed a blocked mouse notification and removed its archived action while leaving the pending request. USBGuard restarted and reused ID 7 with the identical rule; the same watcher recovered a fresh request and visible notification. Repeating action loss followed by a watcher-only restart also restored the prompt with the same daemon/device. Unplugging the test mouse removed its request. Expanded regressions cover concurrent reconnect/policy events, new daemon/watcher generations, legacy requests, and delayed removal. USB and CLI suites, syntax, and whitespace checks pass. Both services remain active and the temporary device is removed.

Firmware Secure Boot validation on e12c8ed

Tested in a separate cold-cloned Lab VM with its own disk, OVMF Secure Boot NVRAM, virtual TPM, and freshly enrolled test keys. The shipped boot helper matched the PR file hash and ran with real Secure Boot detection and the normal sbctl configuration.

  • Firmware enforcement: a signed EFI probe executed; the same unsigned program was rejected with Access Denied -- rejected probably by Secure Boot. Linux also reported Secure Boot enabled and Setup Mode disabled.
  • Actual reboots passed with USB boot protection enabled and disabled on the current root and a supported snapshot. A historical snapshot UKI with a nonempty embedded .cmdline was rewritten and re-signed by the production helper in both directions, then successfully booted with the requested effective USB parameter.
  • With boot protection enabled, an observer immediately before USBGuard recorded both the trusted tablet and unknown mouse unauthorized, no HID driver bound, and hub authorization defaults of zero. USBGuard then allowed the trusted tablet; real input events were read from its device node. The unknown mouse remained unauthorized with no interface driver. Disabling boot protection restored early authorization defaults of one while USBGuard still subsequently blocked the unknown mouse.
  • A real bootloader-signing failure injected after archive preparation during disable made the operation fail. Every preexisting boot-file hash and the boot settings were restored, the restored signature/menu enrollment verified, and the VM rebooted successfully with Secure Boot and the previous USB boot policy still enabled.

Scope: OVMF/QEMU firmware and an unencrypted virtual disk. Physical firmware, USB input at an encrypted-disk prompt, power-loss recovery, and custom multi-profile/PCR-signed images were not tested. The original Lab firmware and keys were not changed. No additional source fix was needed and no fresh Astra rereview is claimed.

Screenshots

Unknown device blocked

USB accessory blocked notification

Review and authorization choices

USB device authorization review

Manual boot-time menu entry

USB at Boot in the Security menu

Original USB at Boot confirmation

USB authorization from boot confirmation showing the connected-device trust policy and encrypted-disk input warning

Updated USB at Boot confirmation, including legacy-snapshot recovery limits

USB boot confirmation with recovery limitations

@acrogenesis acrogenesis changed the title Add opt-in USB device authorization Require approval for new USB devices by default Sep 15, 2026
@acrogenesis
acrogenesis force-pushed the feature/usb-device-authorization branch 6 times, most recently from a07e064 to 0fa3e29 Compare September 15, 2026 02:53
@acrogenesis
acrogenesis marked this pull request as ready for review September 15, 2026 03:34
Co-authored-by: Outfoxxed <outfoxxed@outfoxxed.me>
@acrogenesis
acrogenesis force-pushed the feature/usb-device-authorization branch from 0fa3e29 to 44f68f4 Compare September 15, 2026 04:13
@acrogenesis

Copy link
Copy Markdown
Member Author

Ready to merge @omacom/core pending UX decision:

Should we block devices even if the screen is unlocked (most secure) or only if the screen is locked (best ux)?

@acrogenesis acrogenesis changed the title Require approval for new USB devices by default Require approval for new USB and Thunderbolt devices by default Sep 25, 2026
@acrogenesis
acrogenesis force-pushed the feature/usb-device-authorization branch from a902349 to a3340fd Compare September 25, 2026 21:26
@acrogenesis
acrogenesis force-pushed the feature/usb-device-authorization branch from a3340fd to 60d6b25 Compare September 28, 2026 15:17
@gavrix

gavrix commented Oct 10, 2026

Copy link
Copy Markdown

@acrogenesis this feature just locked me out of my Omarchy, and I had to boot into snapshot to disable USBGuard.

When it blocks both keyboard and mouse and there is literally no way to approve it. I see the notification asking me to click on it... and cant click because both keyboard and mouse are blocked.

PS. I connect both mac and Linux desktop to the same DELL monitor I use as KVM.

acrogenesis added a commit to acrogenesis/omarchy that referenced this pull request Oct 11, 2026
Withdraw the enrollment migrations and install, provisioning, first-run and menu integration from omacom#11874. Retain recovery and approval helpers while a new migration restores already-enrolled machines, including optional boot policy. Preserve unrelated provisioning and first-run fixes.
acrogenesis added a commit to acrogenesis/omarchy that referenced this pull request Oct 11, 2026
Withdraw the enrollment migrations and install, provisioning, first-run and menu integration from omacom#11874. Retain recovery and approval helpers while a new migration restores already-enrolled machines, including optional boot policy. Preserve unrelated provisioning and first-run fixes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants