The problem: SSH gets hammered Link to heading
I keep an SSH server running on every machine I administer, ideally on a non-standard port. I’ve previously written about hardening the SSH configuration itself, but a hardened config doesn’t stop the flood of connection attempts. Let’s look at the scale of it:
quebec:/var/log # ag "Invalid user" auth.log.2 auth.log.3 | wc -l
4560
quebec:/var/log # head -n 1 auth.log.3 | cut -d " " -f 1-3
May 8 07:39:01
quebec:/var/log # tail -n 1 auth.log.2 | cut -d " " -f 1-3
May 21 07:39:01
4,560 invalid login attempts over two weeks (about 325 per day), even with PubkeyAuthentication-only mode enabled. Password auth was already off, so none of these had any chance of succeeding, but they’re still noise, and they could succeed against a less hardened server.
The solution: Fail2ban Link to heading
Fail2ban monitors service log files, tracks which IP addresses are generating bad events, and bans them via iptables when they exceed a configurable threshold. Install it:
# On openSUSE:
zypper in fail2ban
# On Ubuntu/Debian:
apt install fail2ban
But, and this is the critical point: installing Fail2ban without configuring it will not protect you.
You must configure it Link to heading
The default configuration:
# "bantime" is the number of seconds that a host is banned.
bantime = 600
# A host is banned if it has generated "maxretry" events during
# the last "findtime" seconds.
findtime = 600
maxretry = 5
Ten minutes ban, ten minutes window, five attempts. Brute-forcers know these defaults. Here’s one being clever about it:
quebec:/var/log # ag "Invalid user" auth.log.2 auth.log.3 | ag <IP> | cut -d " " -f 1-3
auth.log.2:8106:May 21 03:34:22
auth.log.2:8112:May 21 03:43:41
auth.log.2:8116:May 21 03:53:00
auth.log.2:8120:May 21 04:02:18
auth.log.2:8126:May 21 04:11:47
auth.log.2:8132:May 21 04:21:08
One attempt every ~nine minutes, just inside the 600-second window. Staying under the maxretry threshold forever.
The right approach: override the defaults Link to heading
Never edit /etc/fail2ban/fail2ban.conf directly: it gets overwritten on package upgrades. Instead, create a local override file:
awk '{ printf "# "; print; }' /etc/fail2ban/jail.conf | sudo tee /etc/fail2ban/jail.local
This creates /etc/fail2ban/jail.local with all default settings commented out. Uncomment and adjust the values you want to change. At minimum, set bantime, findtime, and maxretry to values that the scanner above wouldn’t survive:
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 86400 # Ban for 24 hours
findtime = 3600 # Within a 1-hour window
maxretry = 3 # After 3 failures
Further customization Link to heading
A few useful Fail2ban capabilities worth knowing:
Whitelist trusted IPs (your own VPN subnet, home IP, etc.) so you don’t lock yourself out:
ignoreip = 192.168.1.0/24 10.0.0.0/8Email alerts on bans, including whois data (useful for spotting patterns)
WordPress login protection: the WP Fail2ban plugin logs failed WordPress logins to syslog, which Fail2ban can then act on. Fail2ban works with nginx, Apache, vsftpd, and many other services, not just sshd.
Shared blacklists: Fail2ban can report banned IPs to coordinated blocklists, though I’ve never used this in practice.
Limitations Link to heading
Fail2ban bans individual IP addresses. It’s entirely ineffective against distributed brute-force attacks, where each attacking IP sends only a handful of requests. For that class of attack, you need a different approach: firewall allowlists (only permit known IPs to reach SSH), a VPN as the entry point, or a service like Cloudflare with bot detection.
Updated 2026: CrowdSec has emerged as a modern, collaborative alternative to Fail2ban. It shares blocklists across the community, handles distributed attacks better, and has a more flexible action system. Worth evaluating for new deployments.