How to protect SSH, WordPress and mail with fail2ban
fail2ban watches log files and temporarily blocks IP addresses that keep failing to authenticate. This guide walks through a practical setup on a Linux server: sane SSH defaults, a custom jail for WordPress login floods, jails for Postfix and Dovecot, whitelisting your own addresses and inspecting bans. It is written for anyone who runs their own VPS or dedicated server and reads logs occasionally, not full time.
How a jail works: filter, action, log
Everything in fail2ban is a jail, and a jail is just three parts wired together:
- Log: a file or the systemd journal that the jail watches, for example /var/log/auth.log.
- Filter: a set of regular expressions (failregex) that decide which log lines count as a failure and which IP caused it.
- Action: what happens when one IP produces maxretry failures within findtime, normally a firewall rule that drops its traffic for bantime.
Install it from the standard repositories:
apt install fail2ban # Debian, Ubuntu dnf install fail2ban # AlmaLinux, Rocky
Never edit /etc/fail2ban/jail.conf directly, package upgrades overwrite it. Put your changes in /etc/fail2ban/jail.local or in separate files under /etc/fail2ban/jail.d/. Filters live in /etc/fail2ban/filter.d/, one file per filter.
Sensible defaults and the sshd jail
A /etc/fail2ban/jail.local that covers most single-server setups:
[DEFAULT] bantime = 1h findtime = 10m maxretry = 5 bantime.increment = true bantime.maxtime = 1w ignoreip = 127.0.0.1/8 ::1 203.0.113.10 [sshd] enabled = true maxretry = 3 bantime = 2h
bantime.increment doubles the ban every time the same IP comes back, up to one week, which quietly removes persistent bots. On Debian 12 and other distributions without rsyslog there is no auth.log, so tell the sshd jail to read the journal instead: add backend = systemd under [sshd]. Reload and confirm the jail is up:
systemctl reload fail2ban fail2ban-client status sshd
A WordPress jail for wp-login.php floods
Bots hammer wp-login.php and xmlrpc.php with POST requests. The web server access log does not tell you whether a password was correct, but that is fine: a human mistypes twice, a bot posts hundreds of times. Create /etc/fail2ban/filter.d/wordpress-login.conf:
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
Then the jail itself, in /etc/fail2ban/jail.d/wordpress.local:
[wordpress-login] enabled = true port = http,https filter = wordpress-login logpath = /var/log/nginx/access.log maxretry = 5 findtime = 5m bantime = 4h
Two warnings. First, test the filter against the real log before enabling it, and expect matches only on POST lines:
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf
Second, if the site sits behind a reverse proxy or CDN, the first field in the log is the proxy address, and this jail would ban your own proxy. Configure the web server to log the real client IP from X-Forwarded-For first, and only then enable the jail.
Mail: postfix and dovecot jails
fail2ban ships working filters for both, so no custom regex is needed:
[postfix] enabled = true mode = aggressive logpath = /var/log/mail.log [dovecot] enabled = true logpath = /var/log/mail.log
The postfix jail in aggressive mode catches SASL authentication failures on port 25 and the submission ports, plus clients that break the SMTP protocol. The dovecot jail covers IMAP and POP3 logins. Mail brute force tends to be slow and spread out, one attempt per hour per IP is common, so consider raising findtime to 1h for these two jails; the ban budget stays the same, but slow scanners get caught too.
Whitelisting your IPs and inspecting bans
Locking yourself out is the classic fail2ban accident. Put every address you administer from into ignoreip in the [DEFAULT] section, space separated, CIDR notation allowed. Include your office and home IPs and any monitoring system that probes the services.
Day-to-day inspection:
fail2ban-client status # list of jails fail2ban-client status sshd # counters and banned IPs fail2ban-client set sshd unbanip 198.51.100.7 fail2ban-client set sshd banip 203.0.113.99 grep Ban /var/log/fail2ban.log # ban history
After any config change run fail2ban-client reload and check the jail status again; a typo in a filter silently produces a jail that matches nothing.
Why fail2ban does not replace keys-only SSH
fail2ban is reactive. Every attacking IP still gets maxretry minus one free guesses per findtime window, and real attacks are distributed: a botnet with ten thousand addresses gets tens of thousands of password guesses per day without breaking a single limit. If password authentication is enabled and the password is weak, fail2ban only slows the attacker down.
So treat the layers as complementary. Keys-only SSH removes the guessable secret; fail2ban then removes the noise, the CPU wasted on TLS and SSH handshakes, and covers the services that cannot go keys-only, such as mail and WordPress logins. In /etc/ssh/sshd_config:
PasswordAuthentication no KbdInteractiveAuthentication no
The whole setup above takes about half an hour on a fresh server and needs no maintenance afterwards. If you want to practice on a clean machine first, a small VPS is enough: everything here runs fine in 1 GB of RAM, and you can break and rebuild the config without touching production.