Why Did fail2ban Ban Me and Miss the Real Attackers? (and How to Fix Both)

fail2ban locked out your office after one fat-fingered SSH login, while the brute-force flood filling the journal never earned a single ban. Those look like two unrelated bugs. They are the same tool doing exactly what its two halves were configured to do: the whitelist you never wrote, and the log matching that quietly stopped working.

fail2ban is not clever. It reads log lines, matches them against a failregex, counts hits per IP inside a window, and asks the firewall to drop anything over the limit. Every symptom you are seeing lives in one of those steps. The self-ban is the counting step working against a source it should have ignored. The missing bans are the matching step returning nothing to count. Fix them separately and in order.

Stop locking yourself out: ignoreip, properly

fail2ban does not know your office egress from a Shanghai botnet. If someone on your network mistypes a password five times, that IP crosses maxretry like any attacker and gets dropped. The fix is ignoreip: a whitelist of ranges fail2ban will count failures for but never ban.

Free · 4 minutes

Would you survive contact with a determined attacker — or an auditor?

Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.

The mistake I see most is putting ignoreip in the wrong place or trusting a single IPv4 address on a dual-stack box. ignoreip belongs in the [DEFAULT] section of /etc/fail2ban/jail.local so every jail inherits it, and it must list every range a legitimate login can arrive from: office egress, the VPN pool, your monitoring host, and both address families. A server reachable over IPv6 that you only whitelisted in IPv4 will happily ban your own connection the moment it comes in over ::.

# /etc/fail2ban/jail.local
[DEFAULT]
# Never ban our own ranges: office egress, VPN pool, monitoring, both families
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.14 2001:db8:1234::/48

# On Ubuntu 24.04 the log source is the journal, not a flat file
backend = systemd

# Write bans into nftables, not legacy iptables
banaction         = nftables-multiport
banaction_allports = nftables-allports

bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
# The systemd unit is ssh.service on Ubuntu/Debian, not sshd.service
journalmatch = _SYSTEMD_UNIT=ssh.service

ignoreip is space- or comma-separated and takes plain addresses, CIDR blocks, and hostnames. Reload with fail2ban-client reload and confirm with fail2ban-client get sshd ignoreip. Treat this list the way you treat any allowlist that grants standing access: review it, keep it minimal, and take stale ranges out. The same discipline that keeps API keys from becoming a liability applies to a whitelist that decides who can never be blocked.

Why the brute-force flood sails straight past

Now the harder half. Attacks are hammering the box, the journal is full of failed logins, and fail2ban-client status sshd shows zero currently failed and zero banned. The counter is at zero because the matching step is returning nothing. On Ubuntu 24.04 there are three usual causes, and they compound.

The backend is reading the wrong place. Ubuntu 24.04 logs through systemd-journald and ships no /var/log/auth.log by default. A jail set to backend = auto or pointed at a logpath that does not exist reads an empty or non-existent file and matches nothing. The sshd jail needs backend = systemd.

The journalmatch points at the wrong unit. The systemd backend narrows the journal with a journalmatch. On Debian and Ubuntu the OpenSSH service unit is ssh.service, not sshd.service. Ubuntu 24.04 also moved sshd to socket activation, which changes how connections surface in the journal. A journalmatch filtering on _SYSTEMD_UNIT=sshd.service can therefore see none of the real attempts. Match on ssh.service, and verify the unit name on your own box with systemctl status ssh before trusting it.

The failregex no longer fits the log line. OpenSSH wording drifts between releases. A filter written for an older Failed password for invalid user format, or one that never anticipated an IPv6 source address in the <HOST> position, silently fails to match. The jail is running, the log has attacks, and the regex shrugs.

Test the regex before you trust it

Do not guess which of the three it is. fail2ban-regex replays real log lines through a filter and tells you exactly what matched, without issuing a single ban. It is the only honest way to know whether your filter sees the attack. Test the filter against the same journal the jail reads, not a hand-picked sample.

# Replay the last 5000 ssh journal lines through the sshd filter (dry run)
fail2ban-regex \
  "journalctl -u ssh -o short-iso --no-pager -n 5000" \
  /etc/fail2ban/filter.d/sshd.conf

# Or read the journal directly with the jail's own journalmatch
fail2ban-regex --journalmatch "_SYSTEMD_UNIT=ssh.service" \
  systemd-journal /etc/fail2ban/filter.d/sshd.conf

Read the summary. Lines: 5000 lines, [nnn] matched with real IPs listed under the matched failregex means the matching step works and the problem is elsewhere. 0 matched against a journal you can see is full of failures means the filter is the fault: wrong unit, wrong backend, or a stale failregex. This is the same principle as a pre-commit baseline that actually runs rather than one you assume runs. Configuration you have not exercised against real input is a guess wearing a config file.

Confirm the ban lands in the kernel

A matched failregex still is not a ban. fail2ban has to hand the address to the firewall, and on Ubuntu 24.04 that firewall is nftables. If your jail is on the default iptables action but the host runs pure nftables, the ban can be recorded by fail2ban yet never enforced against the attacker. Set banaction = nftables-multiport and then check the kernel is actually holding the rules.

# Is the jail up and counting?
fail2ban-client status sshd

# Did the ban reach nftables? Look for the fail2ban table and set
nft list table inet f2b-table

fail2ban-client status sshd should show a rising failure count and a non-empty banned list once real traffic is matching. nft list table inet f2b-table should show the banned addresses in the jail’s set. Seeing a ban in fail2ban-client but nothing in nft means the action is misconfigured and the attacker is still getting through. This is exactly the kind of gap that only shows up when you measure the outcome rather than the intent, which is the whole argument for instrumenting what actually happens instead of trusting that a tool did its job.

Work the chain in order every time: whitelist your own ranges, prove the filter matches real attacks with fail2ban-regex, then confirm the ban reaches nftables. A fail2ban that jails you and waves the attackers through is not broken software. It is unverified software, running unread.

Build and rescue work

Hands-on delivery of this kind is handled by Sixteen Pillars Studio.

Free interactive tool

Website compliance checklist

What your site has to do, based on what it actually does

Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.

Most technology problems are not technology problems. They are control problems.

The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.