#!/usr/sbin/nft -f # Generated by edgeguard-api — DO NOT EDIT. # Source: internal/firewall/firewall.go. # Re-generate via `edgeguard-ctl render-config` or via API mutations. flush ruleset table inet edgeguard { set peer_ipv4 { type ipv4_addr; flags interval {{- if .PeerIPv4}} elements = { {{range $i, $ip := .PeerIPv4}}{{if $i}}, {{end}}{{$ip}}{{end}} } {{- end}} } set peer_ipv6 { type ipv6_addr; flags interval {{- if .PeerIPv6}} elements = { {{range $i, $ip := .PeerIPv6}}{{if $i}}, {{end}}{{$ip}}{{end}} } {{- end}} } chain input { type filter hook input priority 0; policy drop; # ── ANTI-LOCKOUT (immer aktiv, kann von keiner Custom-Rule overruled werden) ── # nft input-chain wird top-down evaluiert; eine accept-Action terminiert. # Diese Block kommt VOR den Custom-Rules — d.h. selbst wenn ein # Operator versehentlich „drop alles" baut, bleibt SSH + Admin-UI # erreichbar. tcp dport 22 ct state new limit rate 10/minute accept comment "anti-lockout: SSH (rate-limited)" tcp dport 443 accept comment "anti-lockout: HAProxy public HTTPS" tcp dport 3443 accept comment "anti-lockout: Management-UI (HAProxy admin HTTPS)" # Stateful baseline ct state established,related accept ct state invalid drop iif lo accept # ICMP — keep PMTUD and basic diagnostics ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept ip6 nexthdr icmpv6 icmpv6 type { echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept # Public ingress: HAProxy serves :80 (ACME + redirect) tcp dport 80 accept # Cluster-internal: peers reach edgeguard-api over mTLS on :8443 tcp dport 8443 ip saddr @peer_ipv4 accept tcp dport 8443 ip6 saddr @peer_ipv6 accept # ── Operator-defined rules ── {{range .Legs}} # rule {{.RuleID}}{{if .Name}} ({{.Name}}){{end}}{{if .Comment}} — {{.Comment}}{{end}} {{- /* Body MUSS auf EIGENER Zeile starten (nicht via {{- }} an die Comment-Zeile angehängt — sonst frisst nft die rule als Teil des # Kommentars). */ -}} {{""}} {{if .SrcIfaces}}iifname { {{join .SrcIfaces ", "}} } {{end}}{{if .DstIfaces}}oifname { {{join .DstIfaces ", "}} } {{end}}{{if .SrcAddrs}}ip saddr { {{join .SrcAddrs ", "}} } {{end}}{{if .DstAddrs}}ip daddr { {{join .DstAddrs ", "}} } {{end}}{{with .Service}}{{if and (or (eq .Proto "tcp") (eq .Proto "udp")) .PortStart}}{{.Proto}} dport {{.PortStart}}{{if and .PortEnd (ne .PortEnd .PortStart)}}-{{.PortEnd}}{{end}} {{else if eq .Proto "icmp"}}ip protocol icmp {{else if eq .Proto "icmpv6"}}ip6 nexthdr icmpv6 {{end}}{{end}}{{if .Log}}log prefix "edgeguard:{{.RuleID}} " {{end}}{{.Action}} {{end}} } chain forward { type filter hook forward priority 0; policy drop; ct state established,related accept ct state invalid drop # DNAT-rewrites aus prerouting_nat haben den ct.status DNAT-Bit # gesetzt — die müssen forward-passieren dürfen, sonst kommen # Port-Forwards (z.B. :2030 → 10.10.20.12:22) zwar durch das # NAT-Rewrite, scheitern aber an policy=drop. Equivalent zu # iptables -m conntrack --ctstate DNAT. ct status dnat accept # Auto-Forward für SNAT/Masquerade-Origin-Pakete. Forward-chain # sieht das Paket VOR der postrouting-Translation; ct.status ist # also noch nicht "snat". Wir ziehen pro NAT-Rule das SrcCIDR # nach und erlauben new-state-Pakete von dort. Return-Pakete # gehen via ct state established schon durch. {{range .NATRules}}{{if or (eq .Kind "snat") (eq .Kind "masquerade")}}{{if .SrcCIDR}} ip saddr {{.SrcCIDR}} ct state new accept comment "auto-forward for NAT rule {{.ID}}" {{end}}{{end}}{{end}} } chain output { type filter hook output priority 0; policy accept; } chain prerouting_nat { type nat hook prerouting priority -100; {{range .NATRules}}{{if eq .Kind "dnat"}} # NAT {{.ID}} (dnat{{if .Comment}} — {{.Comment}}{{end}}) {{""}} {{/* nft-Syntax: erst L3-match (ip saddr/daddr), DANN L4 (tcp/udp dport). Sonst quittiert der parser '... unexpected ip' an dieser Stelle. */}} {{if .InIfaces}}iifname { {{join .InIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}{{if .DstCIDR}}ip daddr {{.DstCIDR}} {{end}}{{if and .Proto (ne .Proto "any")}}{{.Proto}} {{else}}meta l4proto { tcp, udp } {{end}}{{if .DPortStart}}dport {{.DPortStart}}{{if and .DPortEnd (ne .DPortEnd .DPortStart)}}-{{.DPortEnd}}{{end}} {{end}}{{if .TargetAddr}}dnat to {{.TargetAddr}}{{if .TargetPortStart}}:{{.TargetPortStart}}{{if and .TargetPortEnd (ne .TargetPortEnd .TargetPortStart)}}-{{.TargetPortEnd}}{{end}}{{end}}{{end}} {{end}}{{end}} } chain postrouting_nat { type nat hook postrouting priority 100; # Auto-Hairpin für DNAT-Pakete: alle in prerouting_nat # umgeschriebenen Pakete bekommen zusätzlich SNAT auf die # Box-IP des ausgehenden Iface (masquerade). Sonst antwortet # das DNAT-Ziel via seinem eigenen default-Gateway, das oft # nicht zur EdgeGuard-Box zeigt → SYN_SENT + UNREPLIED. # Trade-off: Backend sieht die Box-IP statt der echten # client-IP (für Logging / Geo-Block: später optional via # NAT-Rule-Flag preserve_client_ip). ct status dnat masquerade {{range .NATRules}}{{if eq .Kind "snat"}} # NAT {{.ID}} (snat{{if .Comment}} — {{.Comment}}{{end}}) {{""}} {{if .OutIfaces}}oifname { {{join .OutIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}{{if .TargetAddr}}snat to {{.TargetAddr}}{{end}} {{end}}{{if eq .Kind "masquerade"}} # NAT {{.ID}} (masquerade{{if .Comment}} — {{.Comment}}{{end}}) {{""}} {{if .OutIfaces}}oifname { {{join .OutIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}masquerade {{end}}{{end}} } }