| 1 |
# Alloy system-service presets |
| 2 |
# |
| 3 |
# systemd reads this at boot / on first user login to decide which |
| 4 |
# units are enabled or disabled by default. Rationale for each entry: |
| 5 |
# |
| 6 |
# - greetd: boot lands on the tuigreet TUI on VT1. |
| 7 |
# - swayosd-libinput-backend: the caps-lock and num-lock overlays. |
| 8 |
# - tailscaled: opt-in only. Enrollment happens via `alloy tail` |
| 9 |
# (see docs/CONTINUITY.md — Alloy does not phone home |
| 10 |
# before the user says yes). |
| 11 |
# |
| 12 |
# The base image ships its browser as an RPM; sandboxed apps are |
| 13 |
# user-installed on demand from a remote the user adds (Alloy names |
| 14 |
# no catalog — see docs/STACK.md), so no first-boot Flatpak |
| 15 |
# provisioning is needed. |
| 16 |
# |
| 17 |
# Format: `enable|disable <unit>`. First match wins; entries without |
| 18 |
# a match fall through to /usr/lib/systemd/system-preset/ defaults. |
| 19 |
|
| 20 |
enable greetd.service |
| 21 |
# Paints the console's ANSI slots with Akari before greetd's tuigreet draws on |
| 22 |
# them, so the first surface a user sees on boot is themed rather than stock. |
| 23 |
# See alloy-vtrgb.service; the map it applies is generated in the Containerfile. |
| 24 |
enable alloy-vtrgb.service |
| 25 |
# Inert unless the kernel command line carries `alloy.installer`, which only |
| 26 |
# the installer ISO sets, so enabling it on every install is safe and keeps |
| 27 |
# the live medium from needing a modified copy of the image. |
| 28 |
enable alloy-installer.service |
| 29 |
# The ssh half of the same flow, and inert by the same flag. It creates the |
| 30 |
# `installer` account that etc/ssh/sshd_config.d/20-alloy-installer.conf names, |
| 31 |
# so a headless machine can be installed from another one. Enabled on every |
| 32 |
# install for the reason above: the live medium is the image, not a modified |
| 33 |
# copy of it. |
| 34 |
# |
| 35 |
# This line is the whole gate. Without the account the sshd drop-in matches |
| 36 |
# nothing, so removing this entry disables the feature rather than opening it — |
| 37 |
# but a line that read `enable` on a unit missing its condition would be the |
| 38 |
# other direction, which is why the condition lives in the unit and not here. |
| 39 |
enable alloy-installer-ssh.service |
| 40 |
# The third piece of the same flow, gated by the same flag, added with the |
| 41 |
# firewall on 2026-08-22. A server-profile medium's zone does not allow mDNS, |
| 42 |
# so without this a headless install medium boots and cannot be found at |
| 43 |
# `<name>.local`. It opens mDNS at runtime only; see the unit. |
| 44 |
enable alloy-installer-firewall.service |
| 45 |
# Lays the console and shop down as layered packages on a freshly installed |
| 46 |
# machine, from the repo that rides inside the image at /usr/share/alloy/rpm, |
| 47 |
# so it needs no network and nothing copied off the medium. They cannot ship |
| 48 |
# inside the image: a component the base carries can never be replaced |
| 49 |
# client-side, so an image carrying them is an image whose console can never be |
| 50 |
# fixed. See alloy-layer-components.service and build/layertest. |
| 51 |
# |
| 52 |
# Enabled on every install, and gated in the unit rather than here. Both of its |
| 53 |
# conditions have to hold — the repo present, and the console absent — so on a |
| 54 |
# machine that already has a console it is a no-op, and on one installed from |
| 55 |
# an older medium it is not its business. Reading "already ran" off the |
| 56 |
# filesystem rather than a stamp is what makes it answer correctly after a |
| 57 |
# rollback or a deliberate uninstall. |
| 58 |
enable alloy-layer-components.service |
| 59 |
# alloy-debug-shell@ is deliberately not listed here. It is a template, and |
| 60 |
# `systemctl preset-all` cannot instantiate one from a preset line: a line |
| 61 |
# naming `alloy-debug-shell@tty9.service` matches no unit file and is ignored |
| 62 |
# without a word, which reads exactly like the feature being broken. Its |
| 63 |
# instances are enabled by name in the Containerfile instead. |
| 64 |
|
| 65 |
# Volume and brightness overlays need no daemon beyond `swayosd-server`, |
| 66 |
# which the sway config exec's, but the lock-key ones do: reading caps-lock |
| 67 |
# and num-lock state means watching the input devices, which a user session |
| 68 |
# cannot do. That is why this half of SwayOSD ships as a system unit, and |
| 69 |
# it is the only part of the package that has a unit at all. Without it the |
| 70 |
# sway config's `bindsym --release Caps_Lock` has nothing behind it, which |
| 71 |
# was the state until 2026-07-21. |
| 72 |
# |
| 73 |
# The privilege is real and worth naming: the daemon opens the libinput |
| 74 |
# devices to watch modifier state. Accepted because the alternative is a |
| 75 |
# bind that silently does nothing, and because it reads state rather than |
| 76 |
# injecting it. |
| 77 |
enable swayosd-libinput-backend.service |
| 78 |
|
| 79 |
# rasdaemon ships its unit disabled, so installing the package is not enough: |
| 80 |
# without this line nothing collects machine-check and EDAC events, and |
| 81 |
# `ras-mc-ctl --error-count` reads zero because zero is what was counted, not |
| 82 |
# what happened. That is the failure mode ECC memory is bought to avoid, and |
| 83 |
# it is silent. Its two siblings need no line here: smartmontools and |
| 84 |
# lm_sensors both enable their own units from %post scriptlets. |
| 85 |
# |
| 86 |
# Cost is a daemon on every install, including laptops with no ECC. It is |
| 87 |
# small, it reads rather than writes, and on a machine with no EDAC driver it |
| 88 |
# simply collects nothing. |
| 89 |
enable rasdaemon.service |
| 90 |
|
| 91 |
# avahi, like sshd below, was already enabled by its own package preset and by |
| 92 |
# nobody's decision. Stated here because it is now load-bearing: a headless |
| 93 |
# machine minted with a baked hostname is found at `<name>.local`, so the |
| 94 |
# responder going away would break the install flow rather than merely remove a |
| 95 |
# convenience. The resolving half is nss-mdns, which needs no unit. |
| 96 |
# |
| 97 |
# The exposure is a multicast responder on every install, answering on the local |
| 98 |
# link only. Bounded by what mDNS is: TTL 1, no routing past the subnet. |
| 99 |
enable avahi-daemon.service |
| 100 |
|
| 101 |
# sshd was already on before this line existed: openssh-server is in |
| 102 |
# fedora-bootc:43 and Fedora enables it in 90-default.preset, which this file |
| 103 |
# never overrode. So the state does not change here. What changes is that it |
| 104 |
# is now a decision (Max, 2026-07-25) rather than something inherited from a |
| 105 |
# server base and never looked at. |
| 106 |
# |
| 107 |
# Kept on because a machine you cannot reach when the display stack is the |
| 108 |
# thing that broke is a machine you debug with a USB stick. That argument is |
| 109 |
# not specific to the bench build host; it is the same on a laptop. |
| 110 |
# |
| 111 |
# The exposure is real and worth stating rather than eliding: a listening |
| 112 |
# daemon on every install, on any network the machine joins. And it is wider |
| 113 |
# than "you need a key" — `sshd -T` on fedora-bootc:43 reports |
| 114 |
# `passwordauthentication yes`. Root is key-only (`permitrootlogin |
| 115 |
# without-password`) and keyboard-interactive is off, but the uid 1000 account |
| 116 |
# `alloy install` creates is reachable with its password from anywhere that can |
| 117 |
# route to the machine. Whether that stays true is a separate decision from |
| 118 |
# this line; see GoingsOn alloy d1fed0d7. |
| 119 |
# |
| 120 |
# Note this is the opposite default from tailscaled below, which is opt-in: |
| 121 |
# tailscale reaches OUT to a service and is a privacy question, sshd listens |
| 122 |
# and is an access question, and they do not resolve the same way. |
| 123 |
enable sshd.service |
| 124 |
|
| 125 |
# firewalld, and it is the enforcement of every exposure comment in this file |
| 126 |
# rather than a new capability. Until 2026-08-22 there was no host firewall at |
| 127 |
# all, so each `enable` line above stated an exposure that nothing bounded: the |
| 128 |
# sshd note two paragraphs up describes a listener reachable from any network |
| 129 |
# the machine joins, and that was literally true. |
| 130 |
# |
| 131 |
# The zone is what the policy is, and the two profiles differ there rather than |
| 132 |
# here: `public` on a client (ssh, mdns, dhcpv6-client, which is exactly what |
| 133 |
# this file enables), Alloy's own `alloy-server` zone on a server, which is the |
| 134 |
# same minus mdns. Both are asserted at build time; see the Containerfile. |
| 135 |
# |
| 136 |
# The exposure this line changes is inbound only. Outbound is untouched, and |
| 137 |
# firewalld is not an application firewall. |
| 138 |
# |
| 139 |
# One interface is exempt and it is deliberate: tailscale0 is bound to the |
| 140 |
# `trusted` zone (etc/firewalld/zones/trusted.xml), because tailscaled writes |
| 141 |
# its own nftables tables and firewalld would otherwise drop every inbound |
| 142 |
# tailnet connection. That file carries the measurement and the cost. |
| 143 |
enable firewalld.service |
| 144 |
|
| 145 |
disable tailscaled.service |
| 146 |
|
| 147 |
# bluetooth was on before this line existed, and that was the problem: Fedora's |
| 148 |
# 90-default.preset carries `enable bluetooth.*`, so every Alloy install has had |
| 149 |
# a Bluetooth daemon by inheritance rather than by decision. Same shape as sshd |
| 150 |
# and avahi above, and stated here for the same reason. |
| 151 |
# |
| 152 |
# Kept on. Alloy is laptop-first, and on a laptop wireless headphones and a |
| 153 |
# wireless mouse are ordinary rather than exotic. The audio half already works |
| 154 |
# and needs nothing here: PipeWire's bluez5 SPA plugins ship in the image, |
| 155 |
# including the LC3 and AAC codecs, so a paired headset plays without further |
| 156 |
# packages. |
| 157 |
# |
| 158 |
# The surface that was missing is `alloy bluetooth`, shipped 2026-08-05. It |
| 159 |
# fronts `bluetoothctl` rather than bluez's D-Bus, which is a correction to what |
| 160 |
# this comment said before: the console's promise is that the log pane shows the |
| 161 |
# argv that ran, and a D-Bus call has no argv to show. bluetuith was the obvious |
| 162 |
# ready-made TUI and is not in the Fedora repositories, so taking it would mean |
| 163 |
# another COPR; not worth it for a view this project builds itself anyway. |
| 164 |
# |
| 165 |
# The verb is not offered on the server profile, where the Containerfile's |
| 166 |
# overlay disables this unit. |
| 167 |
# |
| 168 |
# The exposure is a daemon holding an adapter that can be discoverable. bluez |
| 169 |
# does not make it discoverable on its own — that is a `bluetoothctl` command a |
| 170 |
# user runs — so the default state is a radio that scans when asked and answers |
| 171 |
# nobody. |
| 172 |
enable bluetooth.service |
| 173 |
|
| 174 |
# cups.socket, not cups.service, and the distinction is the whole point: the |
| 175 |
# socket is what makes printing cost nothing on the machines that never print. |
| 176 |
# Nothing runs until something connects to /run/cups/cups.sock, and then systemd |
| 177 |
# starts the daemon. Enabling cups.service instead would buy a daemon on every |
| 178 |
# laptop for a feature most sessions never touch. |
| 179 |
# |
| 180 |
# cups.path goes with it: it watches the spool directory so a job queued while |
| 181 |
# the daemon is down still wakes it. |
| 182 |
# |
| 183 |
# Fedora's 90-default.preset already enables both, so this changes no state. It |
| 184 |
# is here because "printing works" should be an Alloy decision with the |
| 185 |
# driverless-only position behind it (see the Containerfile printing block and |
| 186 |
# docs/STACK.md), rather than a default inherited from a server base and never |
| 187 |
# examined. That is the same correction made for sshd and avahi. |
| 188 |
# |
| 189 |
# The exposure is a unix socket under /run, owned by root and reachable by the |
| 190 |
# local machine only. CUPS' web UI on localhost:631 rides the same daemon and is |
| 191 |
# not a network listener. |
| 192 |
enable cups.socket |
| 193 |
enable cups.path |
| 194 |
|
| 195 |
# The USB keyboard gate, which is what makes deny-unknown safe to arm at all. |
| 196 |
# It drops USB enforcement whenever the machine has zero usable keyboards and |
| 197 |
# puts it back when one appears (usr/bin/alloy-usb-gate, and the udev rule that |
| 198 |
# makes it continuous). |
| 199 |
# |
| 200 |
# Enabled on every client install, including the overwhelming majority where |
| 201 |
# usbguard is not armed and this does nothing. That is safe by construction |
| 202 |
# rather than by luck: the script only ever *starts* usbguard to undo a stop it |
| 203 |
# performed itself, recorded in a stamp file under /run, so on a machine where |
| 204 |
# nobody turned enforcement on it reads the keyboard count and exits. It cannot |
| 205 |
# be the thing that arms deny-everything. |
| 206 |
# |
| 207 |
# Client only. This line matches nothing on the server profile, which is the |
| 208 |
# silent no-op this file warns about above and is deliberate here: that profile |
| 209 |
# has no keyboard by design, so a gate reading "zero keyboards" would hold |
| 210 |
# enforcement off permanently on exactly the machines that need it most. Its |
| 211 |
# recovery path is the provider console. The Containerfile removes the unit |
| 212 |
# there and asserts it is gone, which is what keeps this no-op intentional |
| 213 |
# rather than a rename nobody noticed. |
| 214 |
enable alloy-usb-gate.service |
| 215 |
|