Start here: is fail2ban even seeing the attacks?
Every diagnosis starts with one command, because it separates the two completely different failures: fail2ban is not reading your logs, or fail2ban is reading them and the ban is not taking effect.
- Run "fail2ban-client status sshd". You get a total failure count and a total banned count.
- Check the jail is reading something real: confirm the logpath file actually exists (ls -l /var/log/auth.log) before assuming the filter is wrong.
- Failures 0 while the server is clearly under attack means fail2ban is not reading the log. Go to the backend section below.
- Failures climbing but bans 0 means the filter matches but the ban action is failing. Go to the firewall section.
- Bans climbing but attackers still connecting means the ban is applied somewhere that does not take effect. Go to the chain order and Docker sections.
The most common cause: the systemd journal backend
This is a common one on current distributions and it is not a mistake you made. Debian 12 dropped rsyslog from default installs, and many minimal, container and cloud images do the same, so sshd logs only to the systemd journal and nothing writes /var/log/auth.log. The stock sshd jail points at that file. Ubuntu Server is the usual exception: it still ships rsyslog, so auth.log normally exists there, which is why this failure looks distro-specific and confusing.
The failure is quiet, which is what makes it expensive. The jail still shows as active. fail2ban-client reports it as running. Nothing in the status output says "I am watching a file that does not exist". You only notice because the attacks never stop.
The fix is to tell the jail to read the journal instead of a file. In /etc/fail2ban/jail.local, inside the [sshd] block, set "backend = systemd" and remove or comment out the logpath line, then restart fail2ban and re-check the status. The failure count should start climbing within a minute on any internet facing server.
If you see "Have not found any log file for sshd jail" in /var/log/fail2ban.log, that is the same problem stated out loud, and the same fix applies.
action vs banaction: which one you actually want to change
This distinction confuses a lot of people, and setting the wrong one is a common reason a config change appears to do nothing.
"banaction" names the mechanism that performs the block: iptables-multiport, nftables-multiport, ufw, firewallcmd-ipset and so on. It is the answer to "how should the IP be blocked". This is almost always the setting you want when bans are not taking effect, because the default may not match the firewall you actually run.
"action" is the whole response, which normally combines a banaction with optional extras such as sending mail or reporting. The shorthands you see in configs (action_, action_mw, action_mwl) are just bans plus mail, or bans plus mail plus log lines.
The practical rule: if bans are recorded but the attacker still gets through, change banaction to match your firewall. If you want a different notification behaviour, change action. Setting action when you meant banaction is a config change that silently accomplishes nothing.
On a server running ufw, set "banaction = ufw". On nftables, use nftables-multiport. If you set banaction to iptables-multiport on a system where nftables is the real backend, bans will be written into a table nothing consults.
The filter matches nothing (regex and log format)
If the jail is reading the right source but failures stay at zero, test the filter against the log directly rather than guessing.
Run "fail2ban-regex" against your log and the filter file. It reports how many lines matched and how many were ignored, which turns a guess into a fact in about ten seconds. A filter that matched a previous distribution release will often silently stop matching after an upgrade changes the log line format.
Also check the clock. fail2ban compares timestamps in the log against findtime, so a container or VM whose time zone differs from the log timestamps can read every event as too old to count. Attacks are then matched and immediately discarded as historical.
The ban is applied but the attacker still connects
When bans are recorded and the traffic keeps arriving, the block exists but is being bypassed. There are three usual reasons.
- Chain order. The fail2ban chain is consulted after an earlier ACCEPT rule, so the packet is already allowed before the ban is evaluated. Check the order of your rules, not just their presence.
- Docker. Published container ports are handled in the DOCKER chain, which is evaluated before the fail2ban chain. A ban on the host does not protect a container port published with -p. This surprises people running a web app in Docker behind a jail that looks perfectly healthy.
- The wrong firewall. Bans written with iptables on a system where nftables is authoritative go into a table that is never consulted. Match banaction to the firewall actually in use.
The limit no configuration can fix
Once the mechanics are right, there is a structural limit worth understanding. fail2ban bans a source address after that address fails a threshold number of times. It is per IP by design.
A distributed attack defeats that model without touching your configuration. If each attempt arrives from a different address in a large pool, no single address ever reaches maxretry, so nothing is ever banned. Your logs fill with failures, every jail is healthy, and the ban count stays at zero. This is not a misconfiguration and no combination of findtime and maxretry fixes it.
That is the point at which per host log parsing stops being the right tool. Detecting the pattern rather than the address, and sharing what one server learns with the others, is a different job.
SecAI works at that level: it watches the behaviour across your servers, blocks sources within seconds of a pattern emerging rather than after a per IP threshold, and applies what one server sees to the rest of the fleet. It also does not need you to notice that a jail quietly stopped reading its log.