Skip to main content

max / alloy

3.5 KB · 69 lines History Blame Raw
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