Fail2ban is installed first and that is usually where it stops: the package is in, the service is running, sshd is apparently covered. Six months later it turns out the jail is reading a file that does not exist on this system, and that everything else — mail, the control panel, the login form on the site — was never covered at all.
Let us go through what is worth enabling on an ordinary VPS with a website, which numbers to put there, and how to make sure a jail is working rather than merely present in the config.
First check that sshd catches anything
One command:
sudo fail2ban-client status sshd
The output has lines for Currently failed, Total failed and Total banned. If Total failed is zero on a server that has been on the internet for at least a day, the jail is not working. Compare it against reality:
sudo lastb | wc -l
Thousands of failed attempts in lastb against zeros in Fail2ban mean exactly one thing: the filter is looking in the wrong place.
The most common cause is Debian 12. It no longer installs rsyslog by default, the file /var/log/auth.log simply does not exist, and the stock sshd jail is configured to read that file. There is no error message about it: the service starts, the status prints, the counters stay at zero. The fix is to switch to the systemd journal:
[sshd]
enabled = true
backend = systemd
The other option is to bring rsyslog back as a package, if a plain text auth.log matters to other tools. Ubuntu 24.04 behaves the same way.
Where to put your settings
Leave /etc/fail2ban/jail.conf alone: it is overwritten when the package updates, and every edit made there will quietly disappear one day. Your own settings go into /etc/fail2ban/jail.local — that file is read last and overrides the shared one.
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.25
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
ignoreip with your own address is not a luxury but insurance: locking yourself out with a typo is easier than it sounds. If you have no static address, keep a second way in besides SSH — your provider's console or VNC.
The jail that changes the picture: recidive
An ordinary jail has a short memory: five attempts in ten minutes, an hour of ban, and an hour later it all starts over. A bot lives with that quite happily and will be back tomorrow and the day after. recidive closes precisely that gap: it reads not the system log but Fail2ban's own, which means it bans whoever Fail2ban has already banned.
[recidive]
enabled = true
bantime = 1w
findtime = 1d
maxretry = 5
Read that as "banned five times in a day, gone for a week". The jail needs the file /var/log/fail2ban.log, so if you have moved Fail2ban's own logging to syslog, recidive will need backend = systemd as well.
In practice the effect of this one jail is usually more noticeable than fine-tuning all the others: the regulars fall away and the logs are left with fresh background only.
What else to enable when the server hosts a site
In descending order of usefulness:
nginx-http-authorapache-auth— brute force against basic authentication. Needed if an admin area or a staging site is behind a web server password;nginx-botsearch— scanning for known paths:/wp-login.php,/phpmyadmin,/.env. That is not a break-in but reconnaissance, and it is what precedes everything else;nginx-limit-req— works only iflimit_req_zoneis defined in Nginx itself; without it the jail is enabled and useless;postfix-saslanddovecot— essential if you run your own mail. Password guessing against mailboxes is continuous and usually watched by nobody at all.
[nginx-botsearch]
enabled = true
logpath = /var/log/nginx/access.log
The site's own login form is a separate story. The application has no log of failed logins of its own, and in access.log a failed attempt looks like an ordinary POST with a 200 or 302 — indistinguishable from a successful one by any generic filter. There are two options: make the application write failures to syslog (most content management systems have a plugin for it), or rate-limit requests to the login page. The second is a limiter, not a detector, and it is better to know that in advance.
The numbers
bantime = 10m is the default that makes people write Fail2ban off as useless: ten minutes is enough for a bot to come back. An hour for the first ban plus recidive for a week works far better than permanent bans, which over time turn into a mile-long list of rules.
maxretry = 3 for SSH is a reliable way to lock yourself out. Five attempts in ten minutes cut off brute force just as well.
Slow guessing — one attempt every five minutes — will never fall inside findtime. That is no reason to stretch the window to a day: you will get false positives against your own colleagues. What works against slow guessing is not a threshold but disabled password authentication.
Make sure the ban reaches the packets
Fail2ban only calls an external command. If banaction does not match whatever actually filters traffic, the log will fill with cheerful Ban 198.51.100.7 lines while the packets keep arriving. Debian 12 uses nftables by default, and with UFW enabled the correct choice is banaction = ufw. To check:
sudo nft list ruleset | grep -c f2b
sudo iptables -S | grep f2b
At least one of the two should show rules. And you can test the filter itself without waiting for an attack:
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
The bottom of the output says how many lines matched. Zero means the filter and the log do not meet, and there is no point configuring anything further.
What Fail2ban does not do
It does not protect against distributed brute force: a thousand addresses with one attempt each will never reach a threshold under any settings. It does not fix the cause — a banned address does not undo a weak password. And it knows nothing about reputation: an address that spent yesterday breaking into other people's servers is clean to it until it starts on yours. Hence the combination used in practice: keys instead of passwords, Fail2ban as a noise limiter, and a shared blocklist (CrowdSec) as knowledge of other people's experience.
A separate problem is that all of this needs looking at occasionally. Nobody types fail2ban-client status per jail for weeks on end, and a rising number of bans gets noticed once something has already broken. On the demo pages below the same data sits on one page: the list of jails, who is banned right now, and where the attempts come from.