I run a small setup with fail2ban protecting nginx and SSH. The setup worked fine – fail2ban watches logs, detects attackers, and bans them via firewalld. Nothing exotic.
After accumulating hundreds of bans across my f2b jails, fail2ban-client reload became noticeably slow. The default firewallcmd-multiport action adds one firewall rule per banned IP, so every reload rebuilds hundreds of individual rules.
Enter IPSet, per Claude, instead of one rule per IP, it stores all banned IPs in a kernel-level hash table behind a single firewall rule. Lookup is O(1) regardless of how many IPs are in the set. Fail2ban already ships with firewallcmd-ipset.conf, so the switch is just a config change:
# /etc/jail.d/defaults-debian.conf
[DEFAULT]
banaction = firewallcmd-ipset
banaction_allports = firewallcmd-ipset[actiontype=<allports>]After systemctl restart fail2ban, fail2ban-client reload went instant — even with 300+ IPs banned across three jails.
To verify fail2ban and ipset stay in sync on every login:
echo "=== Fail2Ban IPSet Status ==="
for jail in $(fail2ban-client status | sed -n 's/,//g;s/.*Jail list://p'); do
f2b_count=$(fail2ban-client status "$jail" | awk '/Currently banned/{print $NF}')
ipset_count=$(ipset list "f2b-$jail" 2>/dev/null | awk '/Number of entries/{print $NF}')
printf "%-30s banned: %-6s ←→ ipset entries: %s\n" "$jail" "$f2b_count" "${ipset_count:-0}"
done