provider-firewalls.md

Provider firewalls

Every host’s packet filter is the NixOS firewall (modules/base.nix: on, stateful, only SSH open; roles open their own ports). A provider firewall in front of it (netcup’s firewall policy, a Hetzner Cloud Firewall) is a second layer that lives outside this repo. It must let through at least what the host opens, and, if it doesn’t track connections, the replies to what the host sends. This page is the list, and the reason for each entry, so the rules can be set by hand today and generated through the providers’ APIs later.

Convention: one catch-all rule allows all outbound traffic; every other rule is for inbound TCP or UDP. So the tables below are inbound only.

The NixOS config is the source of truth for inbound ports. Check this page against it after changing a role:

nix eval --json '.#nixosConfigurations."<fqdn>".config.networking.firewall' \
  --apply 'f: { inherit (f) allowedTCPPorts allowedUDPPorts extraCommands; }'

Stateful or not

Per host

“Opened by” is the module that opens the port in the NixOS firewall. Keep the provider rule in step with it.

infra.mymood.at (netcup): moods, sovrn-metrics, sovrn-relay

Proto Port Source Why Opened by
TCP 22 any SSH (deploys, admin) servers modules/base.nix
TCP 25 any Backup MX mxb.eu.sovrn.at (MX 20 for every sovrn domain) sovrn nix/modules/roles/relay.nix
TCP 80 any ACME HTTP-01 challenges, redirects to HTTPS relay, metrics, moods proxy
TCP 443 any moods, infra.sovrn.at (metrics, logs, alerting; cells push here) sovrn roles/metrics.nix, moods nix/modules/proxy.nix
UDP 443 any HTTP/3 for the same sites moods nix/modules/proxy.nix
UDP any source port 53, 123, 24441 Replies to the box’s DNS, NTP and pyzor queries; see UDP replies none (needed only because netcup’s policy doesn’t track UDP)

netcup’s policy as set on 2026-10-08 matches this table. Its default policy also blocked outbound TCP 25, 465 and 587; that rule was deleted on 2026-10-06 (plan, “infra.mymood.at: network”). If netcup re-applies its default, Alertmanager’s mail (Resend, 587) stops.

mx99.eu.sovrn.at (Hetzner): sovrn-cell

No provider firewall today. If one is added, every sovrn cell needs:

Proto Port Source Why Opened by
TCP 22 any SSH servers modules/base.nix
TCP 25 any MX 10 for the cell’s domains sovrn nix/modules/roles/cell.nix
TCP 80 any ACME HTTP-01, redirects sovrn roles/cell-edge.nix
TCP 443 any sovrnd, PDS origins (*.pds99.eu.sovrn.at on mx99), on-demand TLS sovrn roles/cell-edge.nix
TCP 465, 587 any Submission (implicit TLS, STARTTLS) for users’ mail clients sovrn roles/cell.nix
TCP 143, 993 any IMAP (STARTTLS, implicit TLS) sovrn roles/cell.nix
TCP 2525 the relays only (152.53.243.113, 2a0a:4cc0:2000:84c4:c4a5:5fff:fe80:c760) Relay ingress: the backup relay re-delivers queued mail here, unauthenticated, so it must never be open to anyone else sovrn roles/cell.nix (sovrn.cell.relaySources, from the inventory)

ManageSieve (4190) and Stalwart’s HTTP (8080, loopback) stay closed. The relay addresses come from hosts.json; a generated rule should read them from there as well, so a moved relay updates both layers.

infra.rtw.run (Hetzner): forge

No provider firewall today.

Proto Port Source Why Opened by
TCP 22 any SSH servers hosts/infra-rtw-run/services.nix
TCP 23231 any soft-serve: git over SSH servers roles/forge/default.nix
TCP 9418 any git daemon (public read-only clones) servers roles/forge/default.nix
TCP 80, 443 any soft-serve HTTP and kilimanjaro.io behind Caddy, ACME servers roles/forge/default.nix

UDP replies

Needed only behind a firewall that doesn’t track UDP (netcup). Each source port and what breaks without it. None of these fails loudly.

Source port Replies to Without it
53 DNS: the host’s systemd-resolved, and sovrn’s unbound (Stalwart’s resolver, recursing to authoritative servers anywhere) unbound gets no answers, so Stalwart has no DNS and SPF, DMARC, DNSBLs and DANE fail. resolved still works through netcup’s own resolvers, which hides the problem.
123 NTP (systemd-timesyncd) The clock never syncs and drifts. The shared box was 31 s behind until this rule was added (2026-10-08).
24441 pyzor (Stalwart’s spam filter queries public.pyzor.org) Every inbound message waits out a 5 s timeout in the spam filter (bug d4988da).

A future service that queries something over UDP adds its server’s port here. Allowing inbound UDP to the local ephemeral range (32768-60999, net.ipv4.ip_local_port_range) would cover everything at once, but it is broader than needed while the provider can match on source port.

Outbound (reference)

Covered by the catch-all. This list is for checking a provider’s own outbound blocks (netcup’s default policy blocked SMTP ports; Hetzner blocks some on new accounts) or for narrowing the catch-all one day:

Proto Port From To Why
UDP+TCP 53 all any DNS; unbound recurses to authoritative servers directly
UDP 123 all NTP pool Time
UDP 24441 cells, relay public.pyzor.org Spam filter
TCP 443 all any ACME (Let’s Encrypt), Nix caches, R2 (Litestream, backups), Stalwart’s spam-rule and ASN/geo downloads (GitHub), telemetry to infra.sovrn.at, SMTP2GO and other APIs
TCP 2525 cells mail-eu.smtp2go.com All outbound mail goes through SMTP2GO, which DKIM-signs and delivers (so DANE towards recipients is applied by SMTP2GO, not by the cells)
TCP 2525 relay the cells Re-delivery of queued mail to the cells’ relay ingress
TCP 587 infra.mymood.at smtp.resend.com Alertmanager mail

Nothing needs outbound TCP 25: the cells send through SMTP2GO, and the relay delivers only to the cells on 2525. Hetzner blocks outbound 25 and 465 on new Cloud accounts until asked, so this layout keeps working there.

Checking

From the host, after changing a provider rule:

# UDP replies (DNS, IPv4 and IPv6): expect status NOERROR, not "timed out"
nix shell nixpkgs#dig -c dig +time=3 +tries=1 @198.41.0.4 . SOA
nix shell nixpkgs#dig -c dig +time=3 +tries=1 @2001:503:ba3e::2:30 . SOA
# NTP: NTPSynchronized=yes and a packet count above 0
timedatectl show -p NTPSynchronized; timedatectl timesync-status
# Stalwart's resolver (sovrn hosts): expect an answer within a second
host -p 5335 example.com 127.0.0.1

For pyzor, send a test message and compare the Stalwart log’s DMARC and queue.message-queued timestamps: about 0.2 s with a warm cache, 5 s more if pyzor’s replies are dropped. For inbound ports, test from outside (for example check-host.net for TCP 25 and 443).