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