Where fail2ban stops being enough

fail2ban is a good tool and the right first answer. Here is precisely where it runs out, how to push it further yourself, and what it cannot do at any configuration.

8 min read

Start by giving it credit

fail2ban is in every distribution's repositories, installs in one command, and blocks real attackers the same evening. If you have an exposed server with nothing watching it, installing fail2ban is a strictly better position than the one you are in, and nothing below argues otherwise.

This guide is about the gap between "blocks real attackers tonight" and "still correct in a year across a fleet", because that gap is where the surprises live and it is rarely written down.

None of the limits below are bugs. They are consequences of what fail2ban is: a log-parsing daemon designed in an era of single servers and single attackers.

Limit 1: it bans addresses, not ranges

This is the big one. Attack traffic comes from hosting providers where one operator holds a whole block. Ban 203.0.113.47 and the botnet moves to .48 within the hour, and you absorb another full round of attempts before that one is banned too. Across a /24 that is 256 rounds of the threshold you configured.

You can approximate a fix with a custom action that widens each ban to its /24, but you are now maintaining CIDR arithmetic in shell, and getting it wrong means banning something you needed. The tool has no concept of a range as a first-class object.

The measurable effect: on a server under sustained attack, a range-banning defence sees the attempt count fall off a cliff, while an address-banning one sees it plateau at whatever the rotation rate happens to be.

Limit 2: bans expire, and state does not survive

The default bantime is measured in minutes. That is a reasonable default for a shared host where a real user might trip it, and it is the wrong one for an internet-facing server: the same botnet returns tonight and starts over.

You can set bantime to -1 for permanent bans, and you should — but then the second problem appears. The ban list lives in fail2ban's own database and in the running firewall; rebuild the machine, restore from an image, or migrate to a new host and everything the server learned is gone.

There is a third-order version of this too: a server that has been running for two years accumulates a firewall set with tens of thousands of entries, and nobody has ever reviewed whether the oldest are still worth matching against.

Limit 3: every server learns alone

This one is structural and no amount of configuration fixes it. Each of your servers discovers each attacker independently, and the cost of that discovery is the threshold you set — five attempts, ten, whatever it is.

Ten servers, ten times the same attacker, ten times the threshold absorbed. And each of those attempts is a real sshd fork, a real log line, and a real chance that this is the run in which a reused password gets through.

fail2ban has no mechanism for one server's knowledge to reach another. There are community blocklists you can subscribe to, which help, but they are generic and lag by hours to days; nothing tells you that the address hitting server three right now is the one that hit server one this morning.

Limit 4: it fails silently

This is the limit that actually costs people servers, and it gets the least attention.

fail2ban depends on matching log lines with regular expressions. A distribution upgrade changes an sshd message format; the filter stops matching; the jail is still "running" and reports zero bans, which looks exactly like a quiet week. Package updates have been known to reset jail.local. A systemd backend change leaves the journal unread. The service does not come back after a reboot.

In every one of these cases the monitoring you probably have — is the service running? — says yes. The question that would catch it is "has it banned anyone recently, and does that number look like the attack volume I can see in the log?", and almost nobody asks it.

Pushing fail2ban further yourself

If you want to stay with it — a legitimate choice for a small estate — these are the changes that matter most, roughly in order:

  • Set bantime = -1 and increase findtime, so a slow attacker is still caught. Add the recidive jail on top, so repeat offenders get escalated.
  • Add a custom action that expands a ban to its /24, and test the arithmetic carefully against your own address ranges first.
  • Persist bans across reboots with an nftables set marked persistent, or restore them from the database on start.
  • Monitor the ban count, not the service state. Alert when it is zero while the auth log is not.
  • Whitelist your management addresses in ignoreip before anything else, and verify from a second connection that you did it right.

What a managed agent adds

Two things, and the second one is not buildable alone at any configuration.

The first is the operational floor: subnet bans as a first-class concept, bans that are permanent and restored on boot, the real SSH port detected from sshd's configuration rather than assumed, your address whitelisted at install time, and a console that answers "is this still working" — the failure mode that actually bites.

The second is shared reputation. Every protected server contributes to a common database, so an address that attacked someone else last night is already blocked when it reaches you, and you never absorb its first N attempts. A log parser on one host can only ever learn from attacks on that host.

SSH Protector makes its decisions locally on the agent, so protection does not depend on the cloud being reachable — if the connection drops, the local policy keeps running. One server is free forever, which is enough to run it alongside fail2ban for a week and compare what each one caught.

FAQ

Is fail2ban still worth using?
Yes, as a baseline. It is free, it is in every repository, and on a server with nothing else watching it, it will block real attackers the day you install it. The honest caveats are single-address bans against range-rotating botnets, bans that expire by default, no shared knowledge between hosts, and a silent failure mode.
How do I make fail2ban bans permanent?
Set bantime = -1 in the relevant jail. Be aware that the ban list then lives only in fail2ban's database and the running firewall, so a rebuild or image restore loses everything the server learned, and the set grows without anyone reviewing it. Persist the firewall set across reboots as well, or the bans are lost on every restart.
Can fail2ban ban an entire subnet?
Not natively. It bans the address that appeared in the log line. You can write a custom action that widens each ban to its containing /24, but you are then maintaining CIDR arithmetic yourself, and an error there blocks a range you needed. There is no first-class notion of a range in the tool.
How do I check that fail2ban is actually working?
Run fail2ban-client status sshd and look at the banned count, not just whether the service is active. A jail that is running but has banned nobody while the auth log shows failures means the filter has stopped matching — usually after a distribution upgrade changed an sshd message format. Monitor the ban count, not the process.

Run it alongside for a week

Subnet bans, permanent by default, shared reputation and a view of whether it is alive. One server free forever, no card.