#!/bin/sh
# alloy-usb-seed — write this machine's USB policy from the devices it has.
#
# The piece that makes arming usbguard safe, and the reason the preset line
# could not land on its own.
#
# ## What it is for
#
# The image ships `/etc/usbguard/rules.conf` EMPTY, because that is what the
# Fedora package ships and the Containerfile asserts it stays that way: an image
# carrying a policy would be enforcing somebody else's idea of this machine.
# Empty plus `ImplicitPolicyTarget=block` plus `PresentDevicePolicy=apply-policy`
# means the daemon deauthorizes every USB device the moment it starts.
#
# On a desktop that is the keyboard, and the keyboard gate catches it. On a
# laptop it is NOT the keyboard — the internal one is i8042 and USBGuard has no
# jurisdiction over it — so the gate correctly stays shut and the machine comes
# up with its webcam, its fingerprint reader and everything plugged into it
# silently dead. Measured rather than feared: a machine booted armed against the
# empty policy denied its only USB keyboard within a second of the daemon
# starting. A silent denial is the worse failure of the two, because nothing on
# screen connects the dead webcam to a policy decision.
#
# So the first armed boot has to enforce a policy about THIS machine, and the
# only honest source for that is the machine. `usbguard generate-policy` reads
# the bus and emits an allow rule per attached device. It is the one verb that
# does not go through the daemon's IPC socket (measured on usbguard-1.1.4),
# which is exactly why it can run here, before the daemon exists.
#
# ## What it therefore trusts
#
# Everything attached the first time the machine boots. That is the position,
# stated rather than hidden: a machine trusts the peripherals it was set up
# with, and everything after that is a decision the person makes from
# `alloy usb`. The install medium is usually still in a port at that moment and
# will get a rule of its own; it is the user's own medium, and the line names it
# plainly enough to delete.
#
# ## Why it is safe to run on every boot
#
# It refuses to touch a policy that already exists, so it seeds once and is a
# no-op forever after. It is not gated on a stamp file: the question "has this
# machine got a policy" is answered by the policy, and a stamp would let a
# machine whose rules.conf was emptied by hand arm against nothing.
#
# A machine with no USB devices at all generates nothing, and this leaves the
# file empty rather than writing an empty one, so the next boot with something
# attached seeds properly. Arming against an empty policy is refused separately
# and structurally: etc/systemd/system/usbguard.service.d/ carries a
# `ConditionFileNotEmpty` on the rule file, so a machine that reaches this state
# boots unarmed rather than bricked, whatever happens in here.
#
# Dependency-light for the same reason the gate is: sh and usbguard, on the boot
# path, before anything else is up.

set -u

POLICY=/etc/usbguard/rules.conf

say() {
    printf 'alloy-usb-seed: %s\n' "$1" >&2
}

# A policy that already says something is this machine's own answer, and not
# ours to replace. `-s` is the whole test: see the header for why there is no
# stamp file beside it.
if [ -s "$POLICY" ]; then
    say "policy already written, leaving it alone"
    exit 0
fi

command -v usbguard >/dev/null 2>&1 || {
    say "no usbguard on this machine, nothing to seed"
    exit 0
}

# Generated into a temporary file beside the real one rather than redirected
# over it. A redirect truncates before the command runs, so a generate-policy
# that failed halfway would leave a partial policy that looks like a complete
# one — and a partial allow-list is a machine that denies whatever did not make
# it into the file, silently.
#
# Beside it, rather than in /tmp, so the move is a rename within one filesystem
# and therefore atomic. /etc/usbguard is 0700 root:root, so the temporary file
# is no more readable than the policy it becomes.
temporary="$POLICY.seed.$$"
trap 'rm -f "$temporary"' EXIT INT TERM

if ! usbguard generate-policy > "$temporary" 2>/dev/null; then
    say "generate-policy failed; leaving the policy empty, which leaves this machine unarmed"
    exit 0
fi

if [ ! -s "$temporary" ]; then
    say "no USB devices attached, so there is nothing to allow; leaving the policy empty"
    exit 0
fi

# The mode is set on the temporary file rather than after the move, so the
# policy is never briefly readable under its real name.
chmod 0600 "$temporary" 2>/dev/null || true
if ! mv -f "$temporary" "$POLICY" 2>/dev/null; then
    say "could not write $POLICY; this machine stays unarmed"
    exit 0
fi

say "wrote $(grep -c '^allow' "$POLICY" 2>/dev/null) rules for the devices attached to this machine"
exit 0
