Skip to main content

max / alloy

4.7 KB · 109 lines History Blame Raw
1 #!/bin/sh
2 # alloy-usb-seed — write this machine's USB policy from the devices it has.
3 #
4 # The piece that makes arming usbguard safe, and the reason the preset line
5 # could not land on its own.
6 #
7 # ## What it is for
8 #
9 # The image ships `/etc/usbguard/rules.conf` EMPTY, because that is what the
10 # Fedora package ships and the Containerfile asserts it stays that way: an image
11 # carrying a policy would be enforcing somebody else's idea of this machine.
12 # Empty plus `ImplicitPolicyTarget=block` plus `PresentDevicePolicy=apply-policy`
13 # means the daemon deauthorizes every USB device the moment it starts.
14 #
15 # On a desktop that is the keyboard, and the keyboard gate catches it. On a
16 # laptop it is NOT the keyboard — the internal one is i8042 and USBGuard has no
17 # jurisdiction over it — so the gate correctly stays shut and the machine comes
18 # up with its webcam, its fingerprint reader and everything plugged into it
19 # silently dead. Measured rather than feared: a machine booted armed against the
20 # empty policy denied its only USB keyboard within a second of the daemon
21 # starting. A silent denial is the worse failure of the two, because nothing on
22 # screen connects the dead webcam to a policy decision.
23 #
24 # So the first armed boot has to enforce a policy about THIS machine, and the
25 # only honest source for that is the machine. `usbguard generate-policy` reads
26 # the bus and emits an allow rule per attached device. It is the one verb that
27 # does not go through the daemon's IPC socket (measured on usbguard-1.1.4),
28 # which is exactly why it can run here, before the daemon exists.
29 #
30 # ## What it therefore trusts
31 #
32 # Everything attached the first time the machine boots. That is the position,
33 # stated rather than hidden: a machine trusts the peripherals it was set up
34 # with, and everything after that is a decision the person makes from
35 # `alloy usb`. The install medium is usually still in a port at that moment and
36 # will get a rule of its own; it is the user's own medium, and the line names it
37 # plainly enough to delete.
38 #
39 # ## Why it is safe to run on every boot
40 #
41 # It refuses to touch a policy that already exists, so it seeds once and is a
42 # no-op forever after. It is not gated on a stamp file: the question "has this
43 # machine got a policy" is answered by the policy, and a stamp would let a
44 # machine whose rules.conf was emptied by hand arm against nothing.
45 #
46 # A machine with no USB devices at all generates nothing, and this leaves the
47 # file empty rather than writing an empty one, so the next boot with something
48 # attached seeds properly. Arming against an empty policy is refused separately
49 # and structurally: etc/systemd/system/usbguard.service.d/ carries a
50 # `ConditionFileNotEmpty` on the rule file, so a machine that reaches this state
51 # boots unarmed rather than bricked, whatever happens in here.
52 #
53 # Dependency-light for the same reason the gate is: sh and usbguard, on the boot
54 # path, before anything else is up.
55
56 set -u
57
58 POLICY=/etc/usbguard/rules.conf
59
60 say() {
61 printf 'alloy-usb-seed: %s\n' "$1" >&2
62 }
63
64 # A policy that already says something is this machine's own answer, and not
65 # ours to replace. `-s` is the whole test: see the header for why there is no
66 # stamp file beside it.
67 if [ -s "$POLICY" ]; then
68 say "policy already written, leaving it alone"
69 exit 0
70 fi
71
72 command -v usbguard >/dev/null 2>&1 || {
73 say "no usbguard on this machine, nothing to seed"
74 exit 0
75 }
76
77 # Generated into a temporary file beside the real one rather than redirected
78 # over it. A redirect truncates before the command runs, so a generate-policy
79 # that failed halfway would leave a partial policy that looks like a complete
80 # one — and a partial allow-list is a machine that denies whatever did not make
81 # it into the file, silently.
82 #
83 # Beside it, rather than in /tmp, so the move is a rename within one filesystem
84 # and therefore atomic. /etc/usbguard is 0700 root:root, so the temporary file
85 # is no more readable than the policy it becomes.
86 temporary="$POLICY.seed.$$"
87 trap 'rm -f "$temporary"' EXIT INT TERM
88
89 if ! usbguard generate-policy > "$temporary" 2>/dev/null; then
90 say "generate-policy failed; leaving the policy empty, which leaves this machine unarmed"
91 exit 0
92 fi
93
94 if [ ! -s "$temporary" ]; then
95 say "no USB devices attached, so there is nothing to allow; leaving the policy empty"
96 exit 0
97 fi
98
99 # The mode is set on the temporary file rather than after the move, so the
100 # policy is never briefly readable under its real name.
101 chmod 0600 "$temporary" 2>/dev/null || true
102 if ! mv -f "$temporary" "$POLICY" 2>/dev/null; then
103 say "could not write $POLICY; this machine stays unarmed"
104 exit 0
105 fi
106
107 say "wrote $(grep -c '^allow' "$POLICY" 2>/dev/null) rules for the devices attached to this machine"
108 exit 0
109