Files
edgeguard-native/internal/firewall/ruleset.nft.tpl
Debian 1740cb7ae2 feat(crowdsec): CrowdSec IDS/IPS Management — v1.2.61
- Backend: internal/crowdsec/service.go — vollständige cscli-Wrapper
  (Decisions, Alerts, Bouncers, Machines, Collections, ServiceStatus)
- Handler: 12 REST-Endpoints mit Audit-Logging unter /crowdsec/*
- Migration 0036: crowdsec_settings-Tabelle
- postinst: CrowdSec-Auto-Install (crowdsec + crowdsec-firewall-bouncer-nftables)
  inkl. sudoers-Einträge für alle cscli-Operationen
- systemd: /var/lib/crowdsec in ReadWritePaths
- UI: CrowdSec-Page mit StatusStrip + 5 Tabs (Decisions, Alerts, Bouncers,
  Machines, Collections), Sidebar-Eintrag, i18n EN+DE
- firewall: flush ruleset → flush table inet edgeguard (CrowdSec-nftables-Table
  bleibt bei Firewall-Render erhalten)
- cluster: Firewall-Reload nur bei echter IP-Änderung, nicht bei jedem
  periodischen Secondary-Heartbeat (verhindert nftables-Counter-Reset)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 20:25:48 +02:00

169 lines
8.9 KiB
Smarty

#!/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.
add table inet edgeguard
flush table inet edgeguard
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"
# HTTP/3 läuft als QUIC über UDP/443. HAProxy bindet quic4@:443
# parallel zum TCP-Listener; ohne diese Regel droppt der policy.
udp dport 443 accept comment "anti-lockout: HAProxy HTTP/3 (QUIC)"
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
# Cluster-internal: PG Logical Replication (:5432) + KeyDB Active-Active (:6379)
tcp dport 5432 ip saddr @peer_ipv4 accept
tcp dport 5432 ip6 saddr @peer_ipv6 accept
tcp dport 6379 ip saddr @peer_ipv4 accept
tcp dport 6379 ip6 saddr @peer_ipv6 accept
# ── Service-Auto-Rules (DNS/Squid/WG/...) ──
# Aus dem laufenden Service-State abgeleitet — Operator
# editiert diese nicht. Wenn der Service entfernt/disabled
# wird, ist die Rule beim nächsten Render weg.
{{range .AutoRules}}
{{if .DstIP}}ip daddr {{.DstIP}} {{end}}{{.Proto}} dport {{.Port}} accept comment "auto: {{.Comment}}"
{{end}}
# ── 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}} " group 0 {{end}}counter {{.Action}} comment "egid:{{.RuleID}}"
{{end}}
# ── DEFAULT-DROP LOGGING ───────────────────────────────────────
# Alles was bis hierhin nicht von einer Custom-Rule oder dem
# Anti-Lockout-Block accept'ed wurde, droppt via policy. Wir
# loggen das mit limit 10/second damit Port-Scans den Log nicht
# fluten. UI Firewall-Log zeigt diese als "drop-input".
limit rate 10/second log prefix "edgeguard:drop-input " group 0
}
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}}
# Auto-Forward für WireGuard-Server-Interfaces: Peer-to-Peer-
# Traffic (Peer A → Peer B, beide via dasselbe wg-Interface)
# muss durch die Box forwarded werden. Ohne diese Regel droppt
# policy=drop den Traffic, auch wenn WG ihn korrekt entschlüsselt.
# Return-Pakete gehen bereits via ct state established durch.
{{range .WGServerIfaces}}
iifname "{{.}}" accept comment "auto: WireGuard peer-forward {{.}}"
{{end}}
# Default-Drop-Logging (limit-rated, siehe input-chain).
limit rate 10/second log prefix "edgeguard:drop-forward " group 0
}
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
# Auto-Masquerade für WireGuard site-to-site: VPN-Clients (z. B. Roadwarrior
# mit 192.168.99.3) greifen auf LANs hinter anderen Peers zu (z. B. 10.0.10.0/24).
# Das entfernte Gateway (z. B. Unify Home) sieht als Return-Destination die
# VPN-Client-IP — die es nicht in seiner Routing-Table hat → Reply wird gedroppt.
# Masquerade schreibt die Source auf die lokale Tunnel-IP um; Return-Traffic
# findet so den Weg zurück durch den Tunnel.
{{range .WGSiteMasq}}
oifname "{{.Iface}}" ip saddr {{.VPNNet}} masquerade comment "auto: WireGuard site-to-site masquerade {{.Iface}}"
{{end}}
{{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}}
}
}