#!/usr/sbin/nft -f define in-tcp-ports = { {{ ansible_port|default(22) }} {% if 'MX' in group_names %} , 25 # SMTP {% endif %} {% if 'LDAP-provider' in group_names %} , 636 # ldaps {% endif %} {% if 'IMAP' in group_names %} , 993 # imaps , 4190 # ManageSieve {% endif %} {% if 'MSA' in group_names %} , 587 # submission [RFC4409] , 465 # submission over TLS [RFC8314] {% endif %} {% if 'webmail' in group_names or 'lists' in group_names or 'wiki' in group_names or 'nextcloud' in group_names %} , 80 # HTTP , 443 # HTTP over SSL/TLS {% endif %} } define out-tcp-ports = { 22 , 80 # HTTP , 443 # HTTP over SSL/TLS {% if 'out' in group_names or 'MSA' in group_names %} , 25 # SMTP {% endif %} {% if 'LDAP-provider' in group_names %} , 11371 # OpenPGP HTTP Keyserver , 43 # whois {% elif 'MX' in group_names or 'lists' in group_names or 'nextcloud' in group_names %} , 636 # ldaps {% endif %} {% if 'IMAP' in group_names %} , 2703 # Razor2 {% endif %} } ############################################################################### flush ruleset table inet filter { # blackholes set fail2ban { type ipv4_addr; timeout 10m; } set fail2ban6 { type ipv6_addr; timeout 10m; } chain input { type filter hook input priority 0 policy drop iif lo accept # XXX Bullseye: this is a hack for the lack of reqid matches in # nftables: we mark the esp packet and accept after decapsulation # https://serverfault.com/questions/971735/how-to-match-reqid-in-nftables # https://blog.fraggod.net/2016/09/25/nftables-re-injected-ipsec-matching-without-xt_policy.html define IPsec.mark = 0x220 meta l4proto esp mark set mark | $IPsec.mark accept ip saddr 172.16.0.0/24 ip daddr 172.16.0.7 mark & $IPsec.mark == $IPsec.mark accept # rate-limiting is done directly by the kernel (net.ipv4.icmp_{ratelimit,ratemask} runtime options) icmp type { echo-reply, echo-request, destination-unreachable } counter accept icmpv6 type { echo-reply, echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem } counter accept # accept neighbour discovery for autoconfiguration, RFC 4890 sec. 4.4.1 icmpv6 type { 133,134,135,136,141,142 } ip6 hoplimit 255 counter accept jump martian jump invalid udp sport 123 udp dport 123 ct state related,established accept {% if groups.all | length > 1 %} udp sport 500 udp dport 500 ct state new,related,established accept {% if groups.NATed | length > 0 %} udp sport 4500 udp dport 4500 ct state new,related,established accept {% endif %} {% endif %} udp sport 53 ct state related,established accept tcp sport 53 ct state related,established accept {% if 'dhclient' in group_names %} udp sport 67 ct state related,established accept {% endif %} meta l4proto tcp ip saddr @fail2ban counter drop meta l4proto tcp ip6 saddr @fail2ban6 counter drop tcp dport $in-tcp-ports ct state related,established accept tcp dport $in-tcp-ports ct state new counter accept tcp sport $out-tcp-ports ct state related,established accept } chain output { type filter hook output priority 0 policy drop oif lo accept # XXX Bullseye: unlike for input we can't use marks here, # because by the time we see a packet to 172.16.0.0/24 we don't # know if it'll be encapsulated meta l4proto esp accept ip saddr 172.16.0.7 ip daddr 172.16.0.0/24 accept meta l4proto { icmp, icmpv6 } accept jump martian jump invalid udp sport 123 udp dport 123 ct state new,related,established accept udp sport 500 udp dport 500 ct state new,related,established accept udp sport 4500 udp dport 4500 ct state new,related,established accept udp dport 53 ct state new,related,established accept tcp dport 53 ct state new,related,established accept {% if 'dhclient' in group_names %} udp dport 67 ct state new,related,established accept {% endif %} tcp sport $in-tcp-ports ct state related,established accept tcp dport $out-tcp-ports ct state related,established accept tcp dport $out-tcp-ports ct state new counter accept meta l4proto tcp counter reject with tcp reset meta l4proto udp counter reject counter reject } chain martian { # bogon filter (cf. RFC 6890 for non-global ip addresses) define invalid-ip = { 0.0.0.0/8 # this host, on this network (RFC 1122 sec. 3.2.1.3) {% if not ansible_default_ipv4.address | ipaddr('10.0.0.0/8') %} , 10.0.0.0/8 # private-use (RFC 1918) {% endif %} , 100.64.0.0/10 # shared address space (RFC 6598) , 127.0.0.0/8 # loopback (RFC 1122, sec. 3.2.1.3) , 169.254.0.0/16 # link local (RFC 3927) {% if not ansible_default_ipv4.address | ipaddr('172.16.0.0/12') %} , 172.16.0.0/12 # private-use (RFC 1918) {% endif %} , 192.0.0.0/24 # IETF protocol assignments (RFC 6890 sec. 2.1) , 192.0.2.0/24 # documentation (RFC 5737) {% if not ansible_default_ipv4.address | ipaddr('192.168.0.0/16') %} , 192.168.0.0/16 # private-use (RFC 1918) {% endif %} , 198.18.0.0/15 # benchmarking (RFC 2544) , 198.51.100.0/24 # documentation (RFC 5737) , 203.0.113.0/24 # documentation (RFC 5737) , 240.0.0.0/4 # reserved (RFC 1112, sec. 4) , 255.255.255.255/32 # limited broadcast (RFC 0919, section 7) } define invalid-ip6 = { ::1/128 # loopback address (RFC 4291) , ::/128 # unspecified (RFC 4291) , ::ffff:0:0/96 # IPv4-mapped address (RFC 4291) , 100::/64 # discard-only address block (RFC 6666) , 2001::/23 # IETF protocol assignments (RFC 2928) , 2001::/32 # TEREDO (RFC 4380) , 2001:2::/48 # benchmarking (RFC 5180) , 2001:db8::/32 # documentation (RFC 3849) , 2001:10::/28 # ORCHID (RFC 4843) , 2002::/16 # 6to4 (RFC 3056) , fc00::/7 # unique-local (RFC 4193) , fe80::/10 # linked-scoped unicast (RFC 4291) } ip saddr $invalid-ip counter drop ip daddr $invalid-ip counter drop ip6 saddr $invalid-ip6 counter drop ip6 daddr $invalid-ip6 counter drop } chain invalid { ct state invalid counter reject # drop bogus TCP packets tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop # null packets tcp flags != syn ct state new counter drop # SYN-flood attacks tcp flags & (fin|syn|rst|psh|ack|urg) == fin|psh|urg counter drop # XMAS packets tcp flags & (fin|syn) == fin|syn counter drop # bogus tcp flags & (syn|rst) == syn|rst counter drop # bogus } }