| 85 |
85 |
|
# So a close that is followed by another open inside FLAP_WINDOW seconds latches
|
| 86 |
86 |
|
# the gate open and says why on the console. Failing open is the correct
|
| 87 |
87 |
|
# direction for a lockout valve, and a machine that is honestly unprotected and
|
| 88 |
|
- |
# usable beats one thrashing its own USB bus. Whether the rescuing keyboard
|
| 89 |
|
- |
# should instead be allowed permanently on the spot is a real decision with a
|
| 90 |
|
- |
# security cost, and it is filed rather than taken here.
|
|
88 |
+ |
# usable beats one thrashing its own USB bus.
|
| 91 |
89 |
|
#
|
| 92 |
|
- |
# Dependency-light on purpose: sh, udevadm, systemctl. It runs on a machine
|
|
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
|
| 93 |
126 |
|
# whose keyboard has just gone away, which is not the moment to discover that
|
| 94 |
127 |
|
# something it reaches for is missing.
|
| 95 |
128 |
|
|
| 99 |
132 |
|
STAMP=$STATE/usb-gate-open # exists => this script suspended enforcement
|
| 100 |
133 |
|
CLOSED=$STATE/usb-gate-closed # when the gate last resumed it, for the flap guard
|
| 101 |
134 |
|
LATCH=$STATE/usb-gate-latched # exists => flapping, stay open and stop deciding
|
|
135 |
+ |
RESCUED=$STATE/usb-gate-rescued # a permanent rule was already written this latch
|
| 102 |
136 |
|
FLAP_WINDOW=60
|
| 103 |
137 |
|
|
| 104 |
138 |
|
UNIT=usbguard.service
|
|
139 |
+ |
POLICY=/etc/usbguard/rules.conf
|
| 105 |
140 |
|
|
| 106 |
141 |
|
# Every write here is best-effort and nothing is fatal. This is on the recovery
|
| 107 |
142 |
|
# path, and a gate that aborts halfway leaves the machine in the state it was
|
| 148 |
183 |
|
systemctl is-active --quiet "$UNIT" 2>/dev/null
|
| 149 |
184 |
|
}
|
| 150 |
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 |
+ |
keyboard_usb_ids() {
|
|
193 |
+ |
for device in /sys/class/input/input*; do
|
|
194 |
+ |
[ -e "$device" ] || continue
|
|
195 |
+ |
udevadm info --query=property --path="${device#/sys}" 2>/dev/null \
|
|
196 |
+ |
| awk -F= '
|
|
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 | sort -u
|
|
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 |
+ |
allow_keyboards() {
|
|
222 |
+ |
ids=$(keyboard_usb_ids)
|
|
223 |
+ |
[ -n "$ids" ] || return 1
|
|
224 |
+ |
generated=$(usbguard generate-policy 2>/dev/null) || return 1
|
|
225 |
+ |
[ -n "$generated" ] || return 1
|
|
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=$IFS
|
|
232 |
+ |
IFS='
|
|
233 |
+ |
'
|
|
234 |
+ |
for id in $ids; do
|
|
235 |
+ |
for line in $(printf '%s\n' "$generated" | grep -F "id $id "); do
|
|
236 |
+ |
[ -n "$line" ] || continue
|
|
237 |
+ |
# Idempotent: a second run over the same keyboard must not grow the
|
|
238 |
+ |
# policy file a line at a time.
|
|
239 |
+ |
if [ -e "$POLICY" ] && grep -qxF "$line" "$POLICY" 2>/dev/null; then
|
|
240 |
+ |
continue
|
|
241 |
+ |
fi
|
|
242 |
+ |
printf '%s\n' "$line" >> "$POLICY" 2>/dev/null || { IFS=$saved_ifs; return 1; }
|
|
243 |
+ |
written=$((written + 1))
|
|
244 |
+ |
done
|
|
245 |
+ |
done
|
|
246 |
+ |
IFS=$saved_ifs
|
|
247 |
+ |
|
|
248 |
+ |
[ "$written" -gt 0 ] || return 1
|
|
249 |
+ |
say ""
|
|
250 |
+ |
say "The keyboard you attached has been added to this machine's USB policy"
|
|
251 |
+ |
say "permanently, and enforcement is coming back on with it allowed:"
|
|
252 |
+ |
say ""
|
|
253 |
+ |
for id in $ids; do
|
|
254 |
+ |
say " $id"
|
|
255 |
+ |
done
|
|
256 |
+ |
say ""
|
|
257 |
+ |
say "To undo that, remove its line from $POLICY."
|
|
258 |
+ |
say ""
|
|
259 |
+ |
return 0
|
|
260 |
+ |
}
|
|
261 |
+ |
|
| 151 |
262 |
|
open_gate() {
|
| 152 |
263 |
|
# Order matters: stopping first is what keeps the daemon from re-denying
|
| 153 |
264 |
|
# each device as the loop below authorizes it.
|
| 174 |
285 |
|
# it — so the next run would see a state it thinks it never made and leave
|
| 175 |
286 |
|
# it there.
|
| 176 |
287 |
|
if systemctl start "$UNIT" 2>/dev/null; then
|
| 177 |
|
- |
rm -f "$STAMP" 2>/dev/null || true
|
|
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 |
+ |
rm -f "$STAMP" "$RESCUED" 2>/dev/null || true
|
| 178 |
291 |
|
say "USB device authorization is back on: a keyboard is present."
|
| 179 |
292 |
|
else
|
| 180 |
293 |
|
say "A keyboard is present, but USB device authorization could not be"
|
| 205 |
318 |
|
say "turning enforcement back on denies it again. Rather than do that in"
|
| 206 |
319 |
|
say "a loop, it is left off."
|
| 207 |
320 |
|
say ""
|
| 208 |
|
- |
say "To keep this keyboard and turn enforcement back on:"
|
|
321 |
+ |
say "The keyboard is being re-authorized now. As soon as the kernel sees"
|
|
322 |
+ |
say "it again, it is added to the policy permanently and enforcement comes"
|
|
323 |
+ |
say "back on by itself, with the keyboard working."
|
|
324 |
+ |
say ""
|
|
325 |
+ |
say "If that does not happen, enforcement stays off and this is the way in:"
|
| 209 |
326 |
|
say ""
|
| 210 |
327 |
|
say " alloy usb"
|
| 211 |
328 |
|
say ""
|
| 225 |
342 |
|
# the latch survived forever and the gate never protected that machine
|
| 226 |
343 |
|
# again: enforcement on, valve permanently disabled, and nothing said so.
|
| 227 |
344 |
|
if armed; then
|
| 228 |
|
- |
rm -f "$LATCH" "$STAMP" 2>/dev/null || true
|
|
345 |
+ |
rm -f "$LATCH" "$STAMP" "$RESCUED" 2>/dev/null || true
|
|
346 |
+ |
elif [ "$keyboards" -gt 0 ] && [ ! -e "$RESCUED" ]; 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 |
+ |
: > "$RESCUED" 2>/dev/null || true
|
|
353 |
+ |
if allow_keyboards; then
|
|
354 |
+ |
rm -f "$LATCH" 2>/dev/null || true
|
|
355 |
+ |
close_gate
|
|
356 |
+ |
else
|
|
357 |
+ |
say ""
|
|
358 |
+ |
say "The keyboard could not be added to the USB policy, so device"
|
|
359 |
+ |
say "authorization stays off. See: alloy usb"
|
|
360 |
+ |
say ""
|
|
361 |
+ |
fi
|
|
362 |
+ |
exit 0
|
| 229 |
363 |
|
else
|
| 230 |
364 |
|
exit 0
|
| 231 |
365 |
|
fi
|