host_firewall: log dropped inbound packets with a global rate limit and reject outbound TCP 25 except on the mail host

This commit is contained in:
2026-10-24 12:14:47 +00:00
parent 41dbce3641
commit 88c30eb252
2 changed files with 35 additions and 4 deletions
+8
View File
@@ -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]
+27 -4
View File
@@ -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 %}
}