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
- Hetzner Cloud Firewall: stateful. Replies to outbound traffic are allowed automatically, so the inbound rules below plus the outbound catch-all are all it needs. None is active today.
- netcup firewall policy: behaves as stateless for UDP (observed 2026-10-08). Replies to the box’s UDP queries arrive on a random high port and were dropped until rules allowed them by source port. TCP replies get through. So a netcup host also needs the UDP reply rules.
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).