| 1 |
# Drop USB enforcement while this machine has no keyboard. |
| 2 |
# |
| 3 |
# Step 4 of the `alloy usb` work. The reasoning, the measurements and the |
| 4 |
# ordering all live in usr/bin/alloy-usb-gate; this file is only what runs it. |
| 5 |
# |
| 6 |
# Two triggers, and both are needed. |
| 7 |
# |
| 8 |
# The boot-time one is this unit's [Install]. It answers the machine that boots |
| 9 |
# with its keyboard already denied — the state a person actually arrives in, |
| 10 |
# since the policy was written on a day when a different keyboard was attached. |
| 11 |
# |
| 12 |
# The continuous one is etc/udev/rules.d/70-alloy-usb-gate.rules, which starts |
| 13 |
# this unit on every input add and remove. A keyboard that dies mid-session is |
| 14 |
# the same lockout with a running session behind it, and a boot-only check would |
| 15 |
# already have passed. The rule starts the unit rather than running the script |
| 16 |
# from `RUN+=`: udev kills a RUN program that outlives its event, and the script |
| 17 |
# calls systemctl and udevadm, both of which are the wrong things to do from |
| 18 |
# inside udev's own context. |
| 19 |
# |
| 20 |
# Ordering is loose for the udev trigger and NOT loose for the boot one, and the |
| 21 |
# difference is a lockout. The script only acts when enforcement is already on, |
| 22 |
# because a gate that could arm nothing is what makes it safe to enable |
| 23 |
# everywhere; it asks `systemctl is-active usbguard`. At boot that is a race it |
| 24 |
# used to lose. Measured in qemu on a machine with no keyboard at all |
| 25 |
# (build/vmtest, KEYBOARD=none): this unit started at +10.202s, usbguard started |
| 26 |
# at +10.390s, this unit finished at +10.619s having read `usbguard` as not |
| 27 |
# running and done nothing, and usbguard reached active at +10.708s. Nothing ever |
| 28 |
# re-ran the gate, because the retrigger is an input add or remove and no |
| 29 |
# keyboard was ever plugged in. The machine sat with enforcement on and no way to |
| 30 |
# type, which is the exact failure this whole mechanism exists to prevent. |
| 31 |
# |
| 32 |
# Being WantedBy=basic.target is not ordering: usbguard.service declares no |
| 33 |
# Before=, so nothing held it ahead of a multi-user.target unit. |
| 34 |
# |
| 35 |
# So: After=usbguard.service, ordering only. No Wants= and no Requires=, because |
| 36 |
# this must never be a reason for the daemon to start. `After=` naming a unit |
| 37 |
# that is not in the transaction is a no-op, so on every machine Alloy ships |
| 38 |
# today — usbguard installed, deliberately not enabled — this changes nothing and |
| 39 |
# the gate still reads the keyboard count and exits. |
| 40 |
# |
| 41 |
# It cannot arm anything: the script only ever starts usbguard to undo a stop it |
| 42 |
# performed itself. So enabling this on a machine where enforcement was never |
| 43 |
# turned on is a no-op forever, which is what makes it safe to enable on every |
| 44 |
# client install. |
| 45 |
|
| 46 |
[Unit] |
| 47 |
Description=Suspend USB device authorization while no keyboard is present |
| 48 |
Documentation=https://makenot.work/git/max/alloy |
| 49 |
After=systemd-udevd.service |
| 50 |
Wants=systemd-udevd.service |
| 51 |
# Ordering only, and never a Wants=. See the header: without this the boot-time |
| 52 |
# run reads enforcement as off and silently does nothing on the one machine that |
| 53 |
# needs it most. |
| 54 |
After=usbguard.service |
| 55 |
# On a machine with no usbguard at all the script reads the keyboard count and |
| 56 |
# exits, which is harmless but pointless. The condition says so out loud rather |
| 57 |
# than leaving a unit that runs on every input event to do nothing. |
| 58 |
ConditionPathExists=/usr/bin/usbguard |
| 59 |
|
| 60 |
[Service] |
| 61 |
Type=oneshot |
| 62 |
ExecStart=/usr/bin/alloy-usb-gate |
| 63 |
# The script is best-effort throughout and returns 0 on every path it means to |
| 64 |
# take, so a non-zero here is a real fault worth seeing in the journal. No |
| 65 |
# Restart=: this is edge-triggered, and the next input event runs it again. |
| 66 |
|
| 67 |
[Install] |
| 68 |
WantedBy=multi-user.target |
| 69 |
|