Skip to main content

max / alloy

Pick firewalld, and say which zone on each profile The secureblue review found the image with no host firewall at all, which is the one gap on that list that is a hole rather than a missing mitigation, and the ship task has been blocked on this ruling since it was filed. firewalld, both profiles. The measurement that decides it is not the size: 2.0 MiB plus eight dependencies for 7.3 MiB, and python3 and nftables are already in the image, so the Python daemon everyone cites as the cost is a library load here. It is that Fedora's stock `public` zone allows ssh, mdns and dhcpv6-client, which is exactly what 50-alloy.preset enables on a client. The pick is therefore a zone that already exists rather than a policy someone maintains against the preset, and cups needs no rule at all because loopback is not filtered. `FedoraWorkstation` is named as explicitly not the zone. It opens 1025-65535 on both protocols and it is the desktop spin's default, so "firewalld" without a zone would be a plausible way to ship almost nothing. The server profile takes an Alloy zone with mdns dropped, because that is the one entry where the two listener sets should diverge: avahi answering 5353 on a Hetzner box is not what buys headless discovery on a LAN, and NetworkManager already puts the baked hostname in DHCP option 12 for the lease table. `tailscale0` pinned to `trusted`, and this is the part that would break silently if it were left out. tailscaled writes its own nftables tables and has no firewalld awareness at all -- zero matching strings in the 1.102.3 binary in this image -- and under nftables each base chain returns its own verdict, so a firewalld reject is final regardless of what tailscale accepted in its own table. The whole tree is addressed by tailnet name. Trusting the tailnet is not new: Tailscale SSH already bypasses authorized_keys on these hosts. Ruling only. Nothing is installed or enabled by this commit; that is the ship task, whose done condition now names the zones.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 13:58 UTC
Signed with PGP, not checked
Commit: fcd829ad184d4ac63da87e72477244fc7762cedc
Parent: 366f61c
1 file changed, +20 insertions, -0 deletions
@@ -373,6 +373,26 @@
373 373 Building with `TRIM=keep` keeps the documentation and the translations both. Measured
374 374 and argued in [IMAGE.md](IMAGE.md#what-was-cut-2026-08-17).
375 375
376 + ## Firewall: **firewalld**, ruled 2026-08-22
377 +
378 + **firewalld, on both profiles, with the stock `public` zone on the client and an Alloy-owned zone on the server.** Until this ruling the image had no host firewall at all: `firewalld` was not installed and no nftables ruleset shipped, so every listener the preset enables was reachable from whatever network the machine was on. On a laptop behind NAT that is survivable. On the server profile it is not, and converting the Hetzner boxes to Alloy is a live plan (wiki `host-base-images`).
379 +
380 + Measured on `localhost/alloy:hardening` before picking. firewalld is 2.0 MiB and pulls eight dependencies for 7.3 MiB total, and none of it is a new stack: `python3` and `nftables` are already in the image, so the Python daemon everyone cites as firewalld's cost is a library load here rather than an interpreter. Against a 5 GB image that is not a size argument in either direction, which leaves the decision to the model rather than the byte count.
381 +
382 + The model is what decides it. Fedora's stock `public` zone allows exactly `ssh`, `mdns` and `dhcpv6-client`, and that is precisely the set `50-alloy.preset` enables on a client: sshd on every install, avahi for the `<name>.local` path the headless flow depends on, and nothing else inbound. cups is a unix socket plus `localhost:631` and needs no rule, because loopback is not filtered. So the pick is not a policy someone has to write and maintain against the preset; it is the zone Fedora already ships, matching a listener set Alloy already documents.
383 +
384 + **`FedoraWorkstation` is explicitly not the zone**, and it is the reason to state the zone rather than leave it at "firewalld". That zone opens `1025-65535` on both tcp and udp, which is most of what a firewall is for, and it is the default on Fedora's desktop spin.
385 +
386 + **The server profile gets `alloy-server`: ssh and dhcpv6-client, no mdns.** The listener sets diverge by exactly one entry, and it is the entry that matters on a public IP. avahi stays installed and running on that profile because a headless box on a LAN is found by name, but a machine with an internet-facing address should not answer 5353 to the internet to buy that. Discovery does not depend on it alone: NetworkManager sends the same hostname in DHCP option 12, so the box appears in the router's lease table, and the baked hostname means the name is known in advance anyway. Granting it back on a LAN server is `firewall-cmd --permanent --add-service=mdns --reload`, and the manual says so.
387 +
388 + **`tailscale0` is pinned to the `trusted` zone**, on both profiles, and this is the part that would silently break if it were left out. tailscaled writes its own nftables tables (`ts-input`, `ts-forward`, `ts-postrouting`) and has no firewalld awareness whatsoever: zero matching strings in the 1.102.3 binary in this image. Under nftables every base chain returns its own verdict, so firewalld rejecting a packet in its table is final no matter what tailscale accepted in its own, and the whole tree is addressed by tailnet name. Trusting the tailnet is not a new position: Tailscale SSH already bypasses `authorized_keys` on these hosts, so the ACL is the access control and has been for as long as the tailnet has existed.
389 +
390 + Rejected: **a hand-written nftables ruleset in the image.** Smaller and more legible on the page, and it owns every interaction firewalld already handles. tailscale and netavark both write rules; NetworkManager knows about firewalld zones and knows nothing about a file; and every future service that needs a port becomes an image rebuild instead of a command. Alloy ships no hand-written ruleset anywhere else either, so this would be a bespoke mechanism with exactly one user.
391 +
392 + Rejected: **a firewall on the server profile only.** It is the cheaper half of the work and it defends the machine that is already the least exposed to the case a firewall is for, which is an untrusted LAN. A laptop is on more networks than a server is.
393 +
394 + Not settled here, and filed as its own work: whether `alloy net` should show the zone alongside the interface. firewalld is a NetworkManager-integrated daemon and `alloy net` is an nmcli front, so the state is one `nmcli` field away, but showing it is a console design question rather than part of the pick.
395 +
376 396 ## VPN
377 397
378 398 **WireGuard, through NetworkManager, with no plugin.** NetworkManager 1.54 speaks WireGuard natively, and the plugin packages a search turns up (`NetworkManager-wireguard` and its `-gnome` variant) do not exist in Fedora 43. What is missing without help is key generation: `nmcli` will import a peer config but will not mint one. `wireguard-tools` supplies `wg` and `wg-quick` and is the whole of what this needed.