diff --git a/host_firewall/defaults/main.yml b/host_firewall/defaults/main.yml index 7ca9518..a763e28 100644 --- a/host_firewall/defaults/main.yml +++ b/host_firewall/defaults/main.yml @@ -46,12 +46,13 @@ firewall: forward_guard: {} # Limits on new inbound TCP connections per service, applied to the public and geo ports the host serves (input) and to # DNATed traffic (forward: port forwards and Docker-published ports), after the admin, cluster, VPN, guest and internal - # sources and the public_ips of every inventory host (IPv4 per address, IPv6 per /64) are exempted. Each service has a SYN - # meter with a tier per source (IPv4 /32, IPv6 /64) and a tier per range (IPv4 /24, IPv6 /48) at rate * range_factor. Its - # excess drops with the log prefix fw-rl-syn-addr: (source tier) or fw-rl-syn-range: (range tier); a spoofed source can trigger - # both, so they are evidence only. A handshake meter per source (same rate and burst, no range tier) counts the packet - # that completes a TCP handshake, which a spoofed source cannot send; its excess is logged with fw-rl-hs: and reset. - # Log limit: log.ratelimit_rate. + # sources and the public_ips of every inventory host (IPv4 per address, IPv6 per /64) are exempted. The rate and burst + # below are the service limit, enforced by the handshake meter per source: it counts the packet that completes a TCP + # handshake, which a spoofed source cannot send, and logs the excess with fw-rl-hs: and resets the connection. Each service + # also has a SYN meter at twice the rate and burst, as a flood cap above the service limit so that a real client reaches the + # handshake meter first: a tier per source (IPv4 /32, IPv6 /64) and a tier per range (IPv4 /24, IPv6 /48) at + # rate * range_factor. Its excess drops with the log prefix fw-rl-syn-addr: (source tier) or fw-rl-syn-range: (range tier); + # a spoofed source can trigger both, so they are evidence only. Log limit: log.ratelimit_rate. # name -> {ports: port or list, rate: connections per `per`, per: second|minute (minute), burst: connections (2 * rate), # range_factor_v4, range_factor_v6: override the factors below}; null drops a service, a service is idle unless a port is served. # Values are 3x the highest legitimate rate seen, with a floor of 12 per minute (3x four connections per minute) where diff --git a/host_firewall/templates/host.nft.j2 b/host_firewall/templates/host.nft.j2 index 97e86f3..5c56870 100644 --- a/host_firewall/templates/host.nft.j2 +++ b/host_firewall/templates/host.nft.j2 @@ -51,6 +51,8 @@ set {{ name }} { 'ports': ports | join(', '), 'rate': svc.value.rate, 'burst': burst, + 'syn_rate': svc.value.rate * 2, + 'syn_burst': burst * 2, 'per': per, 'f4': svc.value.range_factor_v4 | default(rl_factor.v4 | default(6), true), 'f6': svc.value.range_factor_v6 | default(rl_factor.v6 | default(3), true), @@ -193,10 +195,12 @@ table inet host { # range (IPv4 /24, IPv6 /48; the per-source rate times range_factor). Admin, cluster, VPN, guest and internal sources # and the public addresses of all inventory hosts (rl_exempt_v4, rl_exempt_v6) are exempt: cluster nodes reach other hosts # with their public address (ArgoCD git fetches, mail submission). The public set is checked only here, no other chain - # trusts it. The SYN meters drop the excess with the log prefixes fw-rl-syn-addr: (per address) and fw-rl-syn-range: (per range), - # which a spoofed source can trigger: evidence only. The handshake meters count the packet that completes a handshake - # (the second original-direction packet of an established connection), which a spoofed source cannot send: the excess is - # logged with fw-rl-hs: and reset. Only the address tier exists there; ranges are left to CrowdSec. + # trusts it. The handshake meters carry the service limit (rate and burst of firewall.ratelimit): they count the packet that + # completes a handshake (the second original-direction packet of an established connection), which a spoofed source cannot + # send, so a real client above the limit is logged with fw-rl-hs: and reset. Only the address tier exists there; ranges are + # left to CrowdSec. The SYN meters are a looser flood cap at twice the rate and burst: they drop the excess with the log + # prefixes fw-rl-syn-addr: (per address) and fw-rl-syn-range: (per range), which a spoofed source can trigger, so they are + # evidence only. Their cap sits above the service limit so that a real client reaches the handshake meter first. {% macro rl_exempt() %} iifname "wt0" return {% for iface in cluster_ifaces + internal_ifaces + guest_ifaces %} @@ -229,10 +233,10 @@ table inet host { {% endfor %} {% for svc in rl_services if svc.input or svc.forward %} chain rl_{{ svc.name }} { - update @rl_{{ svc.name }}_v4 { ip saddr limit rate over {{ svc.rate }}/{{ svc.per }} burst {{ svc.burst }} packets } counter jump rl_drop_addr - update @rl_{{ svc.name }}_v6 { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over {{ svc.rate }}/{{ svc.per }} burst {{ svc.burst }} packets } counter jump rl_drop_addr - update @rl_{{ svc.name }}_v4r { ip saddr & 255.255.255.0 limit rate over {{ svc.rate * svc.f4 }}/{{ svc.per }} burst {{ svc.burst * svc.f4 }} packets } counter jump rl_drop_range - update @rl_{{ svc.name }}_v6r { ip6 saddr & ffff:ffff:ffff:: limit rate over {{ svc.rate * svc.f6 }}/{{ svc.per }} burst {{ svc.burst * svc.f6 }} packets } counter jump rl_drop_range + update @rl_{{ svc.name }}_v4 { ip saddr limit rate over {{ svc.syn_rate }}/{{ svc.per }} burst {{ svc.syn_burst }} packets } counter jump rl_drop_addr + update @rl_{{ svc.name }}_v6 { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over {{ svc.syn_rate }}/{{ svc.per }} burst {{ svc.syn_burst }} packets } counter jump rl_drop_addr + update @rl_{{ svc.name }}_v4r { ip saddr & 255.255.255.0 limit rate over {{ svc.syn_rate * svc.f4 }}/{{ svc.per }} burst {{ svc.syn_burst * svc.f4 }} packets } counter jump rl_drop_range + update @rl_{{ svc.name }}_v6r { ip6 saddr & ffff:ffff:ffff:: limit rate over {{ svc.syn_rate * svc.f6 }}/{{ svc.per }} burst {{ svc.syn_burst * svc.f6 }} packets } counter jump rl_drop_range } {% endfor %}