From b4fad783e402788fb8ca6c561d545402ec48ffda Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Sat, 10 Oct 2026 15:09:49 +0700 Subject: [PATCH] nftables: let containers reach aardvark-dns Caddy could not obtain a certificate on first boot: every request to acme-v02.api.letsencrypt.org timed out. Containers could reach 1.1.1.1:443 by address but resolved no names at all. Container DNS goes to aardvark-dns on the bridge gateway (10.89.10.1:53). That traffic terminates on the host, so it takes the input hook, not forward. Netavark accepts it in its own table, but gitea_filter's input chain has policy drop, and a packet must be accepted by every base chain on the hook. Our drop won. gitea_filter now accepts tcp/udp 53 from the podman subnet to its gateway. Both come from instance metadata, so the ruleset is rendered with envsubst, as the fail2ban jail already is. It is not interface-based because netavark's bridge name (podman1) is not pinned. setup_nftables also validates the rendered file before installing it. Previously it installed first and validated second, so a ruleset that failed to parse stayed in /etc/sysconfig and would fail nftables.service on the next boot, leaving the host with no gitea_filter table. Co-Authored-By: Claude Opus 5.5 --- vm/bootstrap.sh | 17 +++++++++++++++-- vm/nftables/gitea.nft | 11 ++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/vm/bootstrap.sh b/vm/bootstrap.sh index 4636ba3..6eb49fd 100755 --- a/vm/bootstrap.sh +++ b/vm/bootstrap.sh @@ -198,8 +198,21 @@ setup_nftables() { systemctl disable --now firewalld >/dev/null 2>&1 || true systemctl mask firewalld >/dev/null 2>&1 || true - install -m 0600 "${STATE_DIR}/nftables/gitea.nft" /etc/sysconfig/nftables.conf - nft -c -f /etc/sysconfig/nftables.conf || die "nftables ruleset failed validation" + [[ -n "${PODMAN_SUBNET}" && -n "${PODMAN_GATEWAY}" ]] \ + || die "podman-subnet/podman-gateway metadata missing; cannot render the ruleset" + + # Validate BEFORE installing. A ruleset that fails to parse must never land + # in /etc/sysconfig: nftables.service would fail to load it on the next boot + # and the host would come up with no gitea_filter table at all. + local tmp + tmp=$(mktemp) + envsubst '${PODMAN_SUBNET} ${PODMAN_GATEWAY}' < "${STATE_DIR}/nftables/gitea.nft" > "${tmp}" + if ! nft -c -f "${tmp}"; then + rm -f "${tmp}" + die "nftables ruleset failed validation" + fi + install -m 0600 "${tmp}" /etc/sysconfig/nftables.conf + rm -f "${tmp}" systemctl enable --now nftables systemctl reload nftables diff --git a/vm/nftables/gitea.nft b/vm/nftables/gitea.nft index 2f547fd..0405129 100644 --- a/vm/nftables/gitea.nft +++ b/vm/nftables/gitea.nft @@ -1,6 +1,7 @@ #!/usr/sbin/nft -f # -# Host firewall. Installed by vm/bootstrap.sh as /etc/sysconfig/nftables.conf. +# Host firewall. Rendered by vm/bootstrap.sh into /etc/sysconfig/nftables.conf, +# substituting ${PODMAN_SUBNET} and ${PODMAN_GATEWAY}. # # CRITICAL: this file must NEVER contain `flush ruleset`. The stock nftables # config ships with one, and it would wipe podman/netavark's NAT and forward @@ -37,6 +38,14 @@ table inet gitea_filter { # DHCP renewal from the GCE metadata server. udp sport 67 udp dport 68 accept + # Container DNS. aardvark-dns answers on the bridge gateway, so lookups + # from containers terminate on the host and arrive here, not in forward. + # Netavark accepts them in its own table, but a packet has to survive + # every input-hook chain, and this one's drop policy would discard it + # anyway. Without this rule containers resolve nothing -- Caddy cannot + # reach Let's Encrypt and Gitea cannot reach webhook or mirror hosts. + ip saddr ${PODMAN_SUBNET} ip daddr ${PODMAN_GATEWAY} meta l4proto { tcp, udp } th dport 53 accept comment "container DNS" + # Admin SSH: IAP TCP forwarding range only. There is no other path in -- # the VPC firewall enforces the same restriction as the outer layer. ip saddr 35.235.240.0/20 tcp dport 22 accept comment "IAP SSH"