diff --git a/host_firewall/defaults/main.yml b/host_firewall/defaults/main.yml index 98702f1..5f01783 100644 --- a/host_firewall/defaults/main.yml +++ b/host_firewall/defaults/main.yml @@ -45,6 +45,14 @@ firewall: # traffic to other addresses is not filtered. forward_guard: {} ssh_rate_limit: "10/minute burst 5 packets" + # Kernel log of dropped inbound packets (fw-in-drop: ) and of rejected + # outbound mail (fw-out-reject: ), read from the journal by CrowdSec. Each + # rate is one limit shared by all matching packets. + log: + in_rate: "5/second burst 30 packets" + out_rate: "6/minute burst 5 packets" + # Reject outbound TCP 25; the mail group turns it off. + block_smtp_out: true # EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo # ports to the admin set; a load peaks at about 60 MB of kernel memory, too much for a 1 GB host. geo_countries: [at, be, bg, hr, cy, cz, dk, ee, fi, fr, de, gr, hu, ie, it, lv, lt, lu, mt, nl, pl, pt, ro, sk, si, es, se, ch, "no", gb, is, li] diff --git a/host_firewall/templates/host.nft.j2 b/host_firewall/templates/host.nft.j2 index 16ee621..d3caaca 100644 --- a/host_firewall/templates/host.nft.j2 +++ b/host_firewall/templates/host.nft.j2 @@ -50,7 +50,7 @@ table inet host { type filter hook input priority filter; policy drop; ct state established,related accept - ct state invalid drop + ct state invalid jump drop_log iif lo accept icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept @@ -85,8 +85,31 @@ table inet host { # VPN peers get the vpn ports, then everything public. iifname "wt0" jump vpn jump public + + # Broadcasts and multicasts are never scans; they stay out of the log. + meta pkttype { broadcast, multicast } drop + jump drop_log } + # A single rule makes the limit global: a flood logs at the limit and the + # rest drops silently. The kernel log line has the iptables LOG fields that + # the CrowdSec iptables-logs parser reads. + chain drop_log { + limit rate {{ firewall.log.in_rate }} log prefix "fw-in-drop: " + drop + } +{% if firewall.block_smtp_out %} + + # Outbound mail goes through the mail host only. Loopback stays open for a + # local MTA; the log lines carry OUT=, which CrowdSec does not count as a scan. + chain output { + type filter hook output priority filter; policy accept; + + oifname != "lo" tcp dport 25 limit rate {{ firewall.log.out_rate }} log prefix "fw-out-reject: " + oifname != "lo" tcp dport 25 reject with tcp reset + } +{% endif %} + chain vpn { {% if list(scrapers.tcp) %} {% if list(scrapers.v4) %} @@ -133,9 +156,9 @@ table inet host { {% endif %} ip saddr @admin_v4 accept ip6 saddr @admin_v6 accept - tcp dport 22 ct state new update @ssh_meter_v4 { ip saddr limit rate over {{ firewall.ssh_rate_limit }} } drop + tcp dport 22 ct state new update @ssh_meter_v4 { ip saddr limit rate over {{ firewall.ssh_rate_limit }} } jump drop_log # One IPv6 client usually holds a whole /64. - tcp dport 22 ct state new update @ssh_meter_v6 { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over {{ firewall.ssh_rate_limit }} } drop + tcp dport 22 ct state new update @ssh_meter_v6 { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over {{ firewall.ssh_rate_limit }} } jump drop_log {% if firewall.geo_countries %} ip saddr @geo_v4 accept ip6 saddr @geo_v6 accept @@ -162,7 +185,7 @@ table inet host { {% if list(zone.icmp) %} {{ match }} {{ icmp }} type { {{ list(zone.icmp) }} } accept {% endif %} - {{ match }} drop + {{ match }} jump drop_log {% endfor %} {% endfor %} }