A fail2ban setup takes ten minutes: install the package, create a jail.local, enable the SSH jail, done. Every tutorial covers those ten minutes. What almost none of them cover is what happens afterwards: how much fail2ban actually catches, which attackers it never sees, and whether a ban achieves anything at all when it gets lifted an hour later.
We have been running fail2ban for months on our own production server (Ubuntu 24.04, fail2ban 1.0.2, OpenSSH 9.6). For this article we didn’t retell the documentation. We analysed the last 31 days of the fail2ban log, replayed different settings against the real timestamps, and tested a ban live from a separate machine.
Four findings up front, because they shape everything else:
1. In 31 days fail2ban counted 84,213 failed attempts from 3,068 different IP addresses and issued 2,527 bans against 967 addresses. 2,101 addresses were never banned once.
2. After 65% of all unbans, the same address was back within 24 hours, with a median of 6 minutes after the ban expired. One address was banned 26 times in three days.
3. The default setting (5 attempts in 10 minutes) catches addresses responsible for 66% of the traffic. With 5 attempts per day it would be 97%.
4. Our configuration has no exceptions at all (
ignoreip). On 25 September a server from our own infrastructure logged three failed attempts in three seconds and came two attempts away from being banned.
The guide comes first, the measurements after. If you only need the commands, there’s a checklist at the end.

What is fail2ban and how does it work?
fail2ban is a small Python daemon that reads log files, looks for patterns of failed logins and bans the source address for a while if it shows up too often. It is not a virus scanner, not a firewall and not a full-blown intrusion detection system. It is an automatic bouncer keeping a tally.
Three building blocks work together:
| Component | What it does | File |
|---|---|---|
| Filter | Regular expressions that recognise a log line as a failure and extract the IP | /etc/fail2ban/filter.d/sshd.conf |
| Action | What happens on a ban, usually a firewall rule | /etc/fail2ban/action.d/nftables.conf |
| Jail | Connects filter, action and thresholds (maxretry, findtime, bantime) | /etc/fail2ban/jail.local |
The flow: a log line like Invalid user wallet from 203.0.113.7 arrives. The filter recognises it as a failure and writes Found 203.0.113.7 to the fail2ban log. Once the same address collects at least maxretry hits within findtime, the action fires and the address goes into the firewall. After bantime it is removed again.

One thing matters for every number below: fail2ban does not prevent individual attempts. It reacts only after the threshold is reached. On our server the median time between the first counted failure in the window and the ban was 445 seconds. A bot gets its five tries and about seven minutes before the door closes.
For the basics of SSH itself, see What is SSH?.
fail2ban setup: step by step
The following applies to Ubuntu 22.04/24.04 and Debian 12/13. Package names and default paths differ slightly on other distributions; the principle is the same.
Step 1: Install
sudo apt update
sudo apt install fail2ban
On Debian and Ubuntu the service starts automatically after installation. Check:
systemctl status fail2ban
fail2ban-client version
Ours reports 1.0.2. Older tutorials often refer to 0.9 or 0.10. The basic configuration hasn’t changed since, but some options (such as bantime.increment) only exist from 0.11 onwards.
Step 2: Edit the right file
fail2ban reads its configuration in a fixed order:
/etc/fail2ban/jail.conf– package defaults. Never edit it; updates overwrite it./etc/fail2ban/jail.d/*.conf– additions; on Debian this is wheredefaults-debian.conflives./etc/fail2ban/jail.local– your own settings. Wins over everything above.
On Debian and Ubuntu, defaults-debian.conf already contains:
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
backend = systemd
[sshd]
enabled = true
In other words: the SSH jail is already on after installation, reads from the systemd journal and bans via nftables. Many tutorials still tell you to set backend = auto and logpath = /var/log/auth.log. On current Ubuntu and Debian systems /var/log/auth.log often doesn’t exist any more, and the jail then fails to start or sees nothing.
Step 3: Create jail.local
A sensible starting configuration:
[DEFAULT]
# Your own addresses that must never be banned
ignoreip = 127.0.0.1/8 ::1 198.51.100.10
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = 22
198.51.100.10 is a placeholder. Put in the fixed IP you log in from, plus every machine that connects to the server via SSH automatically (backup server, deploy pipeline, monitoring).
Our own server shows how important that line is. It doesn’t have one:
$ fail2ban-client get sshd ignoreip
No IP address/network is ignored
On 25 September at 06:08, fail2ban counted three failed attempts from a server in our own infrastructure, one per second. Probably a script trying several keys in a row. Two more, and that server would have been locked out for two hours. Nothing happened – but only because the script stopped after three attempts, not because we had planned for it.
Step 4: Test the configuration and reload
sudo fail2ban-client -t
sudo systemctl reload fail2ban
-t checks the syntax without touching the running service. We get one harmless warning ('allowipv6' not defined), followed by OK: configuration test is successful. Only then reload.
Step 5: Check what is actually active
This is the step most tutorials skip, and the most important one. What counts is not the file but what the running daemon reports:
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd maxretry
sudo fail2ban-client get sshd findtime
sudo fail2ban-client get sshd bantime
sudo fail2ban-client get sshd actions
On our server:
Status for the jail: sshd
|- Filter
| |- Currently failed: 9
| |- Total failed: 69550
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 2
|- Total banned: 1977
`- Banned IP list: 129.121.143.111 114.30.86.98
And get sshd bantime returns 7200 – two hours – even though the [DEFAULT] block says 1h. The reason: our jail.local also has bantime = 2h in the [sshd] section, and the jail value beats the default. Read only the top of the file and you get it wrong.
We found a second case of this in our own material. Our Linux server setup article shows a jail.local with banaction = ufw. That line isn’t on the server today, and get sshd actions reports nftables. Which version came first can no longer be determined. What fail2ban-client reports is what counts, not what an article or a note says.
Step 6: Test a ban without locking yourself out
Never test by deliberately failing to log in five times. Instead, ban an address from a documentation range by hand and check that it reaches the firewall:
sudo fail2ban-client set sshd banip 192.0.2.44
sudo nft list set inet f2b-table addr-set-sshd
sudo fail2ban-client set sshd unbanip 192.0.2.44
On our server 192.0.2.44 appears immediately in the addr-set-sshd set and disappears again after the unban. The resulting chain looks like this:
table inet f2b-table {
set addr-set-sshd {
type ipv4_addr
elements = { 114.30.86.98, 129.121.143.111 }
}
chain f2b-chain {
type filter hook input priority filter - 1; policy accept;
tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
}
}
Two details are hidden in there. First: the chain hooks in at priority filter - 1, i.e. before the regular firewall rules. A banned address is blocked even if ufw allows port 22. Second: reject rather than drop. The attacker gets an immediate refusal instead of running into a timeout.
We verified this from outside. We briefly banned our own second server in Helsinki and connected from there:
| External test | Result |
|---|---|
| Port 22 before the ban | open |
| Port 22 during the ban | Connection refused after 54 ms |
| Port 443 during the ban | open |
Port 22 after unbanip | open, set empty again |
So the ban only covers the jail’s port, not the whole server. A banned bot can still load your website. If you want to block the address completely, you need banaction_allports (more on that below under the recidive jail).
Video: Akamai Developers walk through the configuration including custom jails. The video is older but the fundamentals still hold; on current systems backend = systemd is the better choice over the log files shown there.
Step 7: Check that the filter understands your logs
A jail whose filter doesn’t match the log lines runs without errors and never bans anyone. You only notice if you look. That’s what fail2ban-regex is for:
sudo fail2ban-regex systemd-journal[journalflags=1] /etc/fail2ban/filter.d/sshd.conf
Across our entire available journal:
Lines: 27131 lines, 14709 ignored, 12067 matched, 355 missed
12,067 matches, 14,709 deliberately ignored (mainly the Connection closed line that accompanies every failure and would otherwise double-count) and 355 missed. --print-all-missed shows what slips through:
| Missed line | Count |
|---|---|
kex_exchange_identification: read: Connection reset by peer | 173 |
beginning MaxStartups throttling | 39 |
signature algorithm ssh-rsa not in PubkeyAcceptedAlgorithms | 16 |
banner exchange: ... invalid format | 10 |
Too many authentication failures (user root) | 6 |
Almost all of these are scanners that drop the connection before even sending a username. The default filter runs in normal mode and ignores these lines on purpose. If you want to count them, switch the filter to aggressive:
[sshd]
enabled = true
filter = sshd[mode=aggressive]
Whether that’s worth it is a judgement call. 355 out of roughly 12,400 relevant lines is just under 3%. We’ve kept normal.
What fail2ban actually saw in 31 days
The systemd journal on our server only goes back about 19 hours for SSH. The fail2ban log, on the other hand, is rotated weekly and keeps five generations. So for the question “what happened over the past weeks”, fail2ban is ironically our longest memory. We analysed every Found, Ban and Unban line from 30 August to 30 September 2026.
| Metric | Value |
|---|---|
Counted failures (Found) | 84,213 |
| Distinct sources | 3,068 |
Bans (Ban) | 2,527 |
| Distinct banned addresses | 967 |
| Never-banned addresses | 2,101 (68%) |
| Their share of failures | 30.8% |
| IPv6 sources | 0 |
| Memory used by the daemon (PSS) | approx. 32 MB |
A wave starting on 24 September
Daily figures were unremarkable until 23 September, mostly between 300 and 900 failures a day with occasional spikes around 3,000. Then:
| Date | Failures | Bans |
|---|---|---|
| 23 Sep | 568 | 20 |
| 24 Sep | 2,968 | 32 |
| 25 Sep | 7,932 | 575 |
| 26 Sep | 16,158 | 891 |
| 27 Sep | 10,033 | 84 |
| 28 Sep | 13,649 | 163 |
| 29 Sep | 9,802 | 39 |
Since 24 September, 66,131 of the 84,213 failures arrived – almost 80% of the month in one week. The peak hour was 26 September between 10:00 and 11:00 with 1,635 hits.
What the bots were trying shows up in the last 19 hours of the journal: 12,059 login attempts with non-existent usernames. The ten most common:
blockchain, wallet, etherscan, solana, usdt, trustvault, vault, crypto, trustwallet, contract – roughly 1,000 each.
10,426 of the 12,059 attempts (86%) used crypto-related names. The campaign is evidently looking for servers running wallets or blockchain nodes, hoping for a weak password. The classics ubuntu (218) and deploy (157) trail far behind.
In the same period, the journal shows 815 attempts as root. And exactly 4 successful logins – all by key, all ours.
Why fail2ban catches the wave but not the quiet bots
fail2ban handled the wave well. On the peak days, bans jumped from 20–40 to 575 and 891. The wave’s bots were fast and blew past the 5-in-10-minutes threshold easily.
The regulars are a different story. The four most active addresses that were never banned all month:
| Address (truncated) | Failures | Bans | Median interval |
|---|---|---|---|
| 79.99.x.x | 616 | 0 | 263 s (4.4 min) |
| 2.57.121.x (A) | 518 | 0 | 3,697 s (62 min) |
| 2.57.121.x (B) | 485 | 0 | 4,934 s (82 min) |
| 193.46.x.x | 410 | 0 | 4,697 s (78 min) |
The first address tries every four and a half minutes. In ten minutes that’s two, rarely three attempts – never five. The shortest gap we measured for it was 93 seconds. It reliably stays below the threshold, 616 times in four days.
The two addresses from the same 2.57.121.x network come roughly once an hour, for an entire month. Individually unremarkable, together over 1,000 attempts. That doesn’t look like chance; it looks like a bot farm that knows the common fail2ban defaults and stays just below them.

In August we calculated on the same server that the default settings missed 70.4% of attack traffic. This month it’s 30.8%. The difference isn’t fail2ban; it’s the attacker mix. In August slow bots dominated, in September a loud wave. Any claim like “fail2ban catches X percent” is always a snapshot. It depends on who happens to be knocking.
Which setting catches how much: the replay
We replayed all 84,213 timestamps against different combinations of maxretry and findtime. For each address we checked a sliding window: would it have hit the threshold at any point during the month?
| maxretry | findtime | Addresses that would trigger | Their share of total traffic |
|---|---|---|---|
| 5 | 10 min (default) | 894 (29.1%) | 66.4% |
| 3 | 10 min | 1,338 (43.6%) | 83.8% |
| 5 | 1 h | 1,402 (45.7%) | 87.9% |
| 3 | 1 h | 1,652 (53.8%) | 93.5% |
| 5 | 1 day | 1,784 (58.1%) | 97.1% |
| 3 | 1 day | 1,999 (65.2%) | 98.2% |
| 2 | 1 day | 2,236 (72.9%) | 98.9% |
A word of caution: the right-hand column is not the number of attempts prevented. It only says what share of traffic came from addresses that would have been banned at some point. The attempts before the ban and after it expires still happen. The table shows who a setting catches, not how much it filters out.
Still, the pattern is clear: the time window is the bigger lever. Extending findtime from 10 minutes to one day does more than lowering maxretry from 5 to 3. Slow bots spread their attempts over hours – a short window never sees them.
Our suggested compromise for SSH with key-only login:
[sshd]
enabled = true
maxretry = 5
findtime = 1d
bantime = 1d
With key-only login the risk of locking yourself out is low: a wrong key doesn’t produce an Invalid user entry, and a human rarely makes five mistakes in a day. The ignoreip line for your own automation remains a prerequisite.
We have not applied these values to our server yet. This article describes the current state, and a change to production deserves its own deliberate decision – not a side effect of writing a blog post.
The revolving door: bans that are worthless after an hour
The most striking finding this month isn’t about the threshold, it’s about the end of the ban.
Of 2,525 unbans in the log, the unbanned address was back within 24 hours in 1,641 cases (65%). Median: 6.1 minutes after the unban. The bots don’t even notice they were banned – they keep going, hit Connection refused for two hours, and continue when the door reopens.
How often the same address was banned:
| Bans per address | Addresses |
|---|---|
| 1 | 571 |
| 2 | 124 |
| 3–5 | 106 |
| 6–10 | 148 |
| more than 10 | 18 |
Top of the list: an address banned 26 times between 25 and 28 September, with 321 failures along the way. Every single ban worked. In total, fail2ban kept this bot busy on a two-hour cycle for three days without ever getting rid of it.

There are two built-in answers to this.
Answer 1: bantime.increment
Since version 0.11, fail2ban can automatically extend ban times for repeat offenders:
[DEFAULT]
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 4w
Each further ban of the same address then lasts twice as long as the previous one: 2 h, 4 h, 8 h, 16 h … up to four weeks.
A trap we found on our own server: fail2ban remembers previous bans in its SQLite database, which is purged after dbpurgeage. Ours:
$ fail2ban-client get dbpurgeage
Current database purge age is:
`- 86400seconds
One day. When we looked, the database held only 47 bans, while the log shows 2,527. With this setting bantime.increment forgets every repeat offender after 24 hours. If you enable the option, you have to extend the memory as well:
# /etc/fail2ban/fail2ban.local
[DEFAULT]
dbpurgeage = 30d
Answer 2: the recidive jail
The older and very robust approach: a second jail that doesn’t read the SSH logs but the fail2ban log itself. Anyone who shows up there as Ban often enough gets blocked on all ports for a long time:
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 5
The bantime, findtime and banaction values are already defined like this in jail.conf; only enabled is missing. We calculated what it would have done for us:
- 185 addresses collected at least five bans within a day during the month and would have been locked out completely for a week.
- Those exact 185 addresses sent 26,337 further failures in the week after that point – 31% of the entire month’s traffic.
A third of the noise comes from a small group that had already been caught five times. With maxretry = 3 in the recidive jail it would be 221 addresses.

The recidive jail is not enabled on our server either. Together with findtime = 1d and dbpurgeage, it’s on our list for a deliberate change.
fail2ban and the firewall: ufw, nftables, iptables
A common question is whether fail2ban works with ufw. It does, in two ways.
Option A (default on Debian/Ubuntu): fail2ban creates its own nftables table f2b-table and hooks it in before ufw. ufw is untouched and ufw status doesn’t show fail2ban’s bans. That’s our setup. You just need to know where to look: nft list table inet f2b-table.
Option B: banaction = ufw. fail2ban then inserts each ban as a ufw rule (ufw insert 1 deny from ...). Pro: everything in one place. Con: during waves like 26 September you end up with hundreds of rules, and every change goes through ufw.
If you haven’t set up ufw yet, see our ufw firewall setup guide. It also explains why a block rule inserted in the wrong position can be completely ineffective – a problem fail2ban deliberately avoids with insert 1.
More jails: Nginx, Caddy and friends
SSH is the default jail, but fail2ban ships 95 filters on our system, from apache-auth via nginx-http-auth to postfix. A jail for Nginx basic auth:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
Two caveats:
- Behind a reverse proxy or Cloudflare, the log often contains the proxy’s address rather than the visitor’s. fail2ban then bans the proxy – and with it every visitor. Make sure the real client IP ends up in the log first. How proxies pass addresses along is covered in What is a reverse proxy?.
- For Caddy, fail2ban has no ready-made filter. Caddy writes JSON logs; you have to build a filter yourself and test it with
fail2ban-regexagainst real lines.
Video: Learn To HomeLab installs and configures fail2ban step by step on a current Linux system.
Is fail2ban enough to protect SSH?
No, and that’s not a criticism of fail2ban. It’s about putting it in its place.
Our server’s SSH configuration (sshd -T):
passwordauthentication no
kbdinteractiveauthentication no
permitrootlogin without-password
maxauthtries 6
That means there is simply no password to guess. The 12,059 attempts with wallet, solana and ubuntu have zero chance, whether fail2ban bans them or not. fail2ban changes practically nothing about this server’s security. What it does change:
- Less noise in the logs. Banned addresses stop producing log lines, so real anomalies stand out.
- Less load. Every SSH handshake costs CPU. At 16,000 attempts a day that’s measurable, if not dramatic.
- An early warning system. The ban curve shows a wave like the one on 24 September at a glance.
Where fail2ban genuinely protects you: everywhere passwords do exist. Web application login forms, mail servers with SMTP AUTH, FTP, an admin area behind basic auth. There, fail2ban is often the only brake on mass guessing.
The right order for SSH:
- Disable password login, keys only (guide in What is SSH?).
- Firewall with “deny all, allow only what’s needed” (ufw setup).
- fail2ban as the third line, against the noise.
For the rest of the baseline hardening steps, see our Linux server setup article.
fail2ban vs. CrowdSec vs. sshguard
| fail2ban | CrowdSec | sshguard | |
|---|---|---|---|
| Language | Python | Go | C |
| Approach | Reads logs, bans locally | Reads logs, bans locally and shares blocklists with a community | Reads logs, bans locally |
| Configuration | INI files, very flexible | YAML, scenarios from a hub | Minimal |
| Memory on our server | approx. 32 MB | not measured | not measured |
| Prior knowledge of attackers | none, learns locally | shared blocklist | none |
| Privacy | nothing leaves the server | signals are shared | nothing leaves the server |
CrowdSec is the most interesting competitor: an address that has already been flagged on a thousand other servers can be blocked before it makes its first attempt on yours. Against the slow bots in our table that would probably work better than any findtime setting. The price: you share attack data with an third-party service. We did not test CrowdSec for this article and make no measured claims about it.
sshguard is the lean option for people who really only want to protect SSH and don’t want to touch configuration.
Everyday commands
# Overview of all jails
sudo fail2ban-client status
# Jail details including banned addresses
sudo fail2ban-client status sshd
# Ban / unban an address manually
sudo fail2ban-client set sshd banip 203.0.113.7
sudo fail2ban-client set sshd unbanip 203.0.113.7
# Unban everything if you locked yourself out (via your provider's console)
sudo fail2ban-client unban --all
# Which values are really in effect?
sudo fail2ban-client get sshd maxretry
sudo fail2ban-client get sshd findtime
sudo fail2ban-client get sshd bantime
sudo fail2ban-client get sshd ignoreip
# Last 20 bans in the log
sudo grep " Ban " /var/log/fail2ban.log | tail -20
# The firewall side
sudo nft list table inet f2b-table
If you’ve locked yourself out: almost every VPS provider offers a web console that doesn’t go through SSH. Log in there, run fail2ban-client unban --all, then add your own address to ignoreip.
Checklist: fail2ban setup
apt install fail2ban- Create
/etc/fail2ban/jail.local, don’t editjail.conf - Fill
ignoreipwith your own address and every automated client - Set thresholds; for key-only SSH:
findtime = 1dinstead of10m fail2ban-client -t, thensystemctl reload fail2ban- Use
fail2ban-client get …to check which values are really active - Test ban with
banip 192.0.2.44, checknft list table inet f2b-table, thenunbanip - Run
fail2ban-regexagainst real logs: are therematchedhits? - Deal with repeat offenders: recidive jail or
bantime.incrementwith a longerdbpurgeage - Disable SSH password login – that’s the actual protection
What we didn’t measure
- The replay is a simulation over the fail2ban log’s timestamps. It can’t tell how bots would have behaved if they had been banned earlier. Some might have given up, others might have switched addresses.
- We only see what the filter recognises. The 355 missed lines and any connections that end without a log entry are not in the 84,213.
- One server, one month. A server in a different location, with a different provider or a better-known name sees different attackers. Our own August measurement arrived at a very different ratio.
- CrowdSec and sshguard were not run in parallel.
- We changed nothing permanently. Both test bans (documentation address and our own Helsinki server) were lifted immediately; the set was back to its original state afterwards.
Frequently asked questions
How do I set up fail2ban?
Install with apt install fail2ban, create /etc/fail2ban/jail.local with ignoreip, maxretry, findtime, bantime and an [sshd] section containing enabled = true, verify with fail2ban-client -t and reload with systemctl reload fail2ban. On Debian and Ubuntu the SSH jail is already active after installation.
Where is the fail2ban configuration?
Under /etc/fail2ban/. Defaults are in jail.conf; your own settings belong in jail.local or a file under jail.d/. General daemon settings such as dbpurgeage go in fail2ban.local.
Why is fail2ban not banning anyone?
The three most common reasons: the jail reads the wrong source (logpath = /var/log/auth.log on a system without that file), the filter doesn’t match the log lines (check with fail2ban-regex), or attackers stay below the threshold because findtime is too short. We had bots with over 600 attempts a month that were never banned because they only came every four minutes.
How do I unban an IP address in fail2ban?
sudo fail2ban-client set sshd unbanip 203.0.113.7 for a specific jail, or sudo fail2ban-client unban 203.0.113.7 for all jails. unban --all lifts every ban.
How long does fail2ban ban by default?
jail.conf ships with 10 minutes, maxretry 5 and findtime 10 minutes. Many people set bantime to an hour or more. Ours is two hours – and 65% of unbanned addresses came back within a day, with a median of six minutes.
Does fail2ban work with ufw?
Yes. On Debian and Ubuntu, fail2ban bans via its own nftables table by default, which takes effect before ufw; ufw status won’t show these bans. Alternatively, banaction = ufw adds each ban directly as a ufw rule.
Does fail2ban block the whole server or just SSH?
Only the jail’s ports. We tested it: during the ban, port 22 was closed with Connection refused while port 443 stayed reachable. For a full block you need banaction_allports, as used by the recidive jail.
Do I need fail2ban if SSH only allows keys?
Hardly for SSH security – without a password there’s nothing to guess. fail2ban then mainly reduces log noise and a bit of load. It becomes genuinely important for services with password login, such as web logins or mail servers.
What is the recidive jail?
A second jail that reads the fail2ban log itself and blocks addresses that were banned several times within a day on all ports for a week. In our data it would have caught 185 addresses that went on to produce 31% of the month’s traffic.
Which is better, fail2ban or CrowdSec?
fail2ban works entirely locally and is very flexible. CrowdSec shares blocklists with other users and can block known attackers before their first attempt – at the cost of data leaving your server. We run fail2ban and have not measured CrowdSec ourselves.
Does fail2ban protect against IPv6 attackers?
Yes, fail2ban supports IPv6 since version 0.10. In our 31 days, however, not a single failed attempt came over IPv6 – all 3,068 sources used IPv4.
Conclusion
A fail2ban setup is easy, and it works: every one of the 2,527 bans in our month took effect, and the test ban hit the firewall in 54 milliseconds. The interesting questions are elsewhere.
The defaults are built for fast bots. fail2ban handled the 26 September wave well, with almost 900 bans in a day. The quiet regulars who knock every four minutes or once an hour are invisible to a ten-minute window. The most effective lever is findtime, not maxretry.
And a ban that expires after two hours is just a break for most bots. Two thirds were back within a day. If you take fail2ban seriously, you need a second stage: the recidive jail or bantime.increment – plus a database that remembers for longer than a day.
The most important thing, though, is the order. fail2ban is the third line of defence. The first is making sure there’s nothing for attackers to guess. Our server logged 84,213 failed attempts and 0 successes – and that’s down to one line in sshd_config, not to fail2ban.