max / alloy
| 1 | #!/bin/sh |
| 2 | # alloy-usb-gate — drop USB enforcement while this machine has no keyboard. |
| 3 | # |
| 4 | # Step 4 of the `alloy usb` work, and the part that makes deny-unknown safe to |
| 5 | # turn on at all. USBGuard's default posture (deny anything matching no rule) |
| 6 | # is correct right up to the moment the thing it denies is how the person types, |
| 7 | # and then it is a machine nobody can log into. This is the release valve: |
| 8 | # whenever the machine has zero usable keyboards, enforcement comes off and the |
| 9 | # console says so. |
| 10 | # |
| 11 | # Continuous rather than boot-only, which is why a udev rule drives it as well |
| 12 | # as a boot-time run. A keyboard that dies mid-session is the same lockout with |
| 13 | # a running session behind it, and a check that only ran at boot would sit there |
| 14 | # having already passed. |
| 15 | # |
| 16 | # ## What counts as a keyboard, and it is the one thing worth measuring twice |
| 17 | # |
| 18 | # `ID_INPUT_KEYBOARD`, which udev's own `input_id` builtin sets, and NOT |
| 19 | # `ID_INPUT_KEY`. Measured on fw13 2026-08-22, and the difference is the whole |
| 20 | # check: |
| 21 | # |
| 22 | # ID_INPUT_KEY=1 Power Button |
| 23 | # ID_INPUT_KEY=1 Video Bus |
| 24 | # ID_INPUT_KEY=1 Wireless Radio Control |
| 25 | # ID_INPUT_KEY=1 Consumer Control |
| 26 | # ID_INPUT_KEY=1 ID_INPUT_KEYBOARD=1 AT Translated Set 2 keyboard |
| 27 | # ID_INPUT_KEY=1 ID_INPUT_KEYBOARD=1 8BitDo Retro Keyboard Receiver |
| 28 | # |
| 29 | # Four things on this laptop emit key events and are not keyboards. Counting |
| 30 | # them would keep the gate shut on a machine whose only real keyboard had just |
| 31 | # been denied, which is the inverted failure and the expensive one: the gate |
| 32 | # would report itself working while the machine was already lost. |
| 33 | # |
| 34 | # The same reading answers the i8042 half the ruling asks for, and answers it |
| 35 | # by construction rather than by a special case. The internal Framework |
| 36 | # keyboard is `AT Translated Set 2 keyboard` at `isa0060/serio0`, BUS_I8042 and |
| 37 | # not BUS_USB, so USBGuard has no jurisdiction over it; it is present in the |
| 38 | # input subsystem no matter what the USB policy says, so it is counted, so the |
| 39 | # gate stays shut. A check written over `/sys/bus/usb` instead would see no USB |
| 40 | # keyboard on a Framework and open the gate permanently on every one of them. |
| 41 | # |
| 42 | # A denied USB keyboard is not in this list either, and that is also by |
| 43 | # construction: deauthorizing a device unbinds its interfaces, so it stops |
| 44 | # being an input device at all. So "how many keyboards can this person type on" |
| 45 | # and "how many keyboards does the kernel show" are the same question. |
| 46 | # |
| 47 | # ## Suspending enforcement is not stopping the daemon |
| 48 | # |
| 49 | # Measured on usbguard-1.1.4-1.fc43 rather than assumed, and it is the finding |
| 50 | # that shaped this script: `RestoreControllerDeviceState=false` in the shipped |
| 51 | # config, and the shipped unit has no `ExecStop` at all. Nothing restores device |
| 52 | # authorization when the daemon goes away. So a gate that stopped the daemon and |
| 53 | # stopped there would leave every denied device denied, including the keyboard |
| 54 | # it opened for, and would do nothing whatsoever for the person it exists to |
| 55 | # rescue. |
| 56 | # |
| 57 | # Opening the gate is therefore three acts in this order: stop the daemon, so it |
| 58 | # does not immediately undo the next step; authorize everything on the bus; and |
| 59 | # set `authorized_default` so a keyboard plugged in *after* the gate opened comes |
| 60 | # up usable too. Everything, not just the keyboards — a denied device exposes no |
| 61 | # interfaces, so there is no way to tell which of them was going to be the |
| 62 | # keyboard, and the machine is already in the state this exists to get out of. |
| 63 | # |
| 64 | # ## It never arms anything |
| 65 | # |
| 66 | # The gate closes by starting `usbguard.service`, and it will only ever do that |
| 67 | # to undo an open it performed itself, which is what the stamp file records. On |
| 68 | # a machine where enforcement was never turned on there is no stamp, so this |
| 69 | # script reads the keyboard count and does nothing at all, forever. That is |
| 70 | # deliberate: arming is the person's decision from `alloy usb` (or the preset |
| 71 | # line, once the bench tests have run), and a boot-path script that could turn |
| 72 | # deny-everything on by itself would be a far worse hazard than the one this |
| 73 | # closes. |
| 74 | # |
| 75 | # ## The flap guard |
| 76 | # |
| 77 | # The ruling says enforcement resumes when a keyboard is present. Ordinarily |
| 78 | # that is right and uneventful: the policy was seeded from the devices that were |
| 79 | # attached, so unplugging a known keyboard and plugging it back in resumes |
| 80 | # cleanly. The case it does not cover is a keyboard the policy has never heard |
| 81 | # of. There the sequence is: gate closes, daemon starts, daemon denies the new |
| 82 | # keyboard, keyboard count returns to zero, gate opens. Forever, several times a |
| 83 | # second. |
| 84 | # |
| 85 | # So a close that is followed by another open inside FLAP_WINDOW seconds latches |
| 86 | # the gate open and says why on the console. Failing open is the correct |
| 87 | # direction for a lockout valve, and a machine that is honestly unprotected and |
| 88 | # usable beats one thrashing its own USB bus. |
| 89 | # |
| 90 | # ## Keeping the rescue keyboard, ruled 2026-08-27 |
| 91 | # |
| 92 | # The latch is not the end state any more. Once the gate has latched and the |
| 93 | # keyboard has come back onto the bus, the gate writes a permanent allow rule |
| 94 | # for it and resumes enforcement, so the machine ends up protected and usable |
| 95 | # with no command for the user to find. GoingsOn alloy `94ae2ea0`. |
| 96 | # |
| 97 | # **Only while latched, and that is the whole safety property.** The latch means |
| 98 | # this machine has already demonstrated it has no other keyboard, which is the |
| 99 | # state where an attacker's prerequisite — make every existing keyboard |
| 100 | # disappear — has already been met, so refusing buys nothing; and it is the only |
| 101 | # state where somebody is certainly standing at the machine plugging something |
| 102 | # in. A rule written outside it would hand any keyboard-shaped device a |
| 103 | # permanent allow on a machine that was working fine. |
| 104 | # |
| 105 | # The threat model, recorded so it is not re-litigated: an attacker with |
| 106 | # physical access who can make every existing keyboard disappear gets a |
| 107 | # permanent allow for a keyboard-shaped device. On a laptop the internal i8042 |
| 108 | # keyboard is not removable without opening the machine, so this mostly bites |
| 109 | # desktops and docked machines. Taken deliberately, against leaving a locked-out |
| 110 | # user with enforcement off and a command they can only learn from a console |
| 111 | # message. |
| 112 | # |
| 113 | # It happens on the run after the latch rather than in the same one, because at |
| 114 | # the moment of latching the keyboard is denied and therefore invisible: a |
| 115 | # denied device exposes no interfaces, so there is nothing to write a rule |
| 116 | # about. Opening the gate authorizes it, the kernel re-enumerates it, and that |
| 117 | # is a udev event, which is another run of this script — with the keyboard |
| 118 | # present and identifiable. |
| 119 | # |
| 120 | # One attempt per latch, recorded in RESCUED. If the rule is written and the |
| 121 | # daemon still denies the keyboard, the gate reopens and stays open rather than |
| 122 | # writing another rule every minute at a device it is not actually matching. |
| 123 | # |
| 124 | # Dependency-light on purpose: sh, udevadm, systemctl, and usbguard for the |
| 125 | # one policy write. It runs on a machine |
| 126 | # whose keyboard has just gone away, which is not the moment to discover that |
| 127 | # something it reaches for is missing. |
| 128 | |
| 129 | |
| 130 | |
| 131 | STATE=/run/alloy |
| 132 | STAMP=/usb-gate-open # exists => this script suspended enforcement |
| 133 | CLOSED=/usb-gate-closed # when the gate last resumed it, for the flap guard |
| 134 | LATCH=/usb-gate-latched # exists => flapping, stay open and stop deciding |
| 135 | RESCUED=/usb-gate-rescued # a permanent rule was already written this latch |
| 136 | FLAP_WINDOW=60 |
| 137 | |
| 138 | UNIT=usbguard.service |
| 139 | POLICY=/etc/usbguard/rules.conf |
| 140 | |
| 141 | # Every write here is best-effort and nothing is fatal. This is on the recovery |
| 142 | # path, and a gate that aborts halfway leaves the machine in the state it was |
| 143 | # called to fix. |
| 144 | |
| 145 | # VT1 is where the greeter is, so that is where a person with no keyboard is |
| 146 | # looking. /dev/console is the fallback for a machine that is not using VT1 |
| 147 | # for the greeter, and for an install that has not reached one yet. |
| 148 | |
| 149 | || |
| 150 | || |
| 151 | # The journal gets it too, for the case where the screen scrolled or nobody |
| 152 | # was in the room. |
| 153 | |
| 154 | } |
| 155 | |
| 156 | # How many devices the kernel considers a keyboard. See the header: the property |
| 157 | # is ID_INPUT_KEYBOARD, and ID_INPUT_KEY is the trap. |
| 158 | |
| 159 | count=0 |
| 160 | for; do |
| 161 | [ || continue |
| 162 | if |
| 163 | | ; then |
| 164 | count= |
| 165 | fi |
| 166 | done |
| 167 | |
| 168 | } |
| 169 | |
| 170 | # Undo every deauthorization on the bus, and stop new attachments arriving |
| 171 | # denied. Only ever called with the daemon already stopped. |
| 172 | |
| 173 | for; do |
| 174 | [ && |
| 175 | done |
| 176 | for; do |
| 177 | [ && |
| 178 | done |
| 179 | |
| 180 | } |
| 181 | |
| 182 | |
| 183 | |
| 184 | } |
| 185 | |
| 186 | # The USB ids of every keyboard the kernel can see, one `vvvv:pppp` per line. |
| 187 | # |
| 188 | # Read off the same input-subsystem query the count uses, so the two agree about |
| 189 | # what a keyboard is. A keyboard with no USB ids is an internal one — the i8042 |
| 190 | # on a laptop — and it needs no rule, because USBGuard has no jurisdiction over |
| 191 | # it. It drops out here by having no properties to print rather than by a case. |
| 192 | |
| 193 | for; do |
| 194 | [ || continue |
| 195 | |
| 196 | | |
| 197 | /^ID_INPUT_KEYBOARD=1$/ { keyboard = 1 } |
| 198 | /^ID_VENDOR_ID=/ { vendor = $2 } |
| 199 | /^ID_MODEL_ID=/ { model = $2 } |
| 200 | END { if (keyboard && vendor != "" && model != "") print vendor ":" model } |
| 201 | ' |
| 202 | done | |
| 203 | } |
| 204 | |
| 205 | # Write a permanent allow rule for every keyboard now on the bus. |
| 206 | # |
| 207 | # The rule text comes from `usbguard generate-policy` rather than being composed |
| 208 | # here. It is the one verb that does not go through the daemon's IPC socket |
| 209 | # (measured on usbguard-1.1.4), which is what makes it the only one available at |
| 210 | # this moment: the gate is open, so the daemon is stopped. It also emits exactly |
| 211 | # the fields usbguard matches on, which a rule assembled by hand out of sysfs |
| 212 | # would be guessing at. |
| 213 | # |
| 214 | # Selected by USB id, so what is kept is "this model of keyboard" rather than |
| 215 | # every device attached while the machine was in trouble. `DeviceRulesWithPort` |
| 216 | # is false by default, so the rule is not tied to the socket it was plugged |
| 217 | # into, which is what a keyboard that moves ports needs. |
| 218 | # |
| 219 | # Returns success only if something was actually appended. A failure here leaves |
| 220 | # the gate open, which is the correct direction: unprotected and usable. |
| 221 | |
| 222 | ids= |
| 223 | [ || |
| 224 | generated= || |
| 225 | [ || |
| 226 | |
| 227 | # A rule is one line and lines are what is being iterated, so IFS is set to |
| 228 | # a newline for the loop rather than left at whitespace: every generated |
| 229 | # rule contains spaces and a device name that may contain several. |
| 230 | written=0 |
| 231 | saved_ifs= |
| 232 | IFS=' |
| 233 | ' |
| 234 | for; do |
| 235 | for; do |
| 236 | [ || continue |
| 237 | # Idempotent: a second run over the same keyboard must not grow the |
| 238 | # policy file a line at a time. |
| 239 | if [ && ; then |
| 240 | continue |
| 241 | fi |
| 242 | || { IFS=; ; } |
| 243 | written= |
| 244 | done |
| 245 | done |
| 246 | IFS= |
| 247 | |
| 248 | [ || |
| 249 | |
| 250 | |
| 251 | |
| 252 | |
| 253 | for; do |
| 254 | |
| 255 | done |
| 256 | |
| 257 | |
| 258 | |
| 259 | |
| 260 | } |
| 261 | |
| 262 | |
| 263 | # Order matters: stopping first is what keeps the daemon from re-denying |
| 264 | # each device as the loop below authorizes it. |
| 265 | || |
| 266 | |
| 267 | || |
| 268 | || |
| 269 | |
| 270 | |
| 271 | |
| 272 | |
| 273 | |
| 274 | |
| 275 | } |
| 276 | |
| 277 | |
| 278 | # The timestamp goes down before the start, because the flap it feeds is |
| 279 | # measured from the attempt rather than from a success. |
| 280 | || |
| 281 | || |
| 282 | # The stamp comes off only if the daemon really came back. Removing it |
| 283 | # first and then failing to start would leave a machine with enforcement |
| 284 | # off, the bus authorized, and nothing recording that the gate is what did |
| 285 | # it — so the next run would see a state it thinks it never made and leave |
| 286 | # it there. |
| 287 | if ; then |
| 288 | # RESCUED goes with it: enforcement is back on, so the episode this |
| 289 | # gate was in is over and a future latch gets its own one attempt. |
| 290 | || |
| 291 | |
| 292 | else |
| 293 | |
| 294 | |
| 295 | fi |
| 296 | } |
| 297 | |
| 298 | # A close followed by another open inside the window means the keyboard that |
| 299 | # closed the gate is not in the policy, so resuming denies it and we are back |
| 300 | # here. See the header. |
| 301 | |
| 302 | [ || |
| 303 | closed_at= || |
| 304 | now= || |
| 305 | case "" in |
| 306 | ''|*[!0-9]*) ;; |
| 307 | esac |
| 308 | [ |
| 309 | } |
| 310 | |
| 311 | |
| 312 | || |
| 313 | || |
| 314 | |
| 315 | |
| 316 | |
| 317 | |
| 318 | |
| 319 | |
| 320 | |
| 321 | |
| 322 | |
| 323 | |
| 324 | |
| 325 | |
| 326 | |
| 327 | |
| 328 | |
| 329 | } |
| 330 | |
| 331 | keyboards= |
| 332 | |
| 333 | if [; then |
| 334 | # Latched open after a flap. The only thing that supersedes it is a person |
| 335 | # turning enforcement back on themselves, from `alloy usb` or by hand — |
| 336 | # which means they have dealt with the keyboard the policy did not know, |
| 337 | # and the gate goes back to being an ordinary gate. |
| 338 | # |
| 339 | # Reading that off the daemon rather than off a second stamp matters. An |
| 340 | # earlier version cleared the latch only when the stamp was gone, and the |
| 341 | # stamp is not removed by a person running `systemctl start usbguard`, so |
| 342 | # the latch survived forever and the gate never protected that machine |
| 343 | # again: enforcement on, valve permanently disabled, and nothing said so. |
| 344 | if ; then |
| 345 | || |
| 346 | elif [ && [; then |
| 347 | # The rescue keyboard is back on the bus, because opening the gate |
| 348 | # authorized it. This is the one moment a permanent rule may be |
| 349 | # written: see the header. The stamp goes down before the attempt, so a |
| 350 | # write that appears to work and does not still costs one round rather |
| 351 | # than one per minute forever. |
| 352 | || |
| 353 | if ; then |
| 354 | || |
| 355 | |
| 356 | else |
| 357 | |
| 358 | |
| 359 | |
| 360 | |
| 361 | fi |
| 362 | |
| 363 | else |
| 364 | |
| 365 | fi |
| 366 | fi |
| 367 | |
| 368 | if [; then |
| 369 | if ; then |
| 370 | if ; then |
| 371 | |
| 372 | |
| 373 | else |
| 374 | |
| 375 | fi |
| 376 | fi |
| 377 | # Not armed and no keyboard: nothing to suspend, and nothing to say. The |
| 378 | # machine is in whatever state its owner left it in. |
| 379 | |
| 380 | fi |
| 381 | |
| 382 | if [; then |
| 383 | |
| 384 | fi |
| 385 | |
| 386 | |
| 387 |