Troubleshooting

fail2ban not working: not banning or not blocking

Attackers keep getting in and fail2ban seems to do nothing. First find out which of two problems you have, then work down the matching list. Causes are sorted by how often they turn out to be the culprit.

Updated 15 min readChecked against fail2ban 1.1.1

When fail2ban is not working, it is either not banning (the jail never sees the failures) or banning without blocking (the firewall rule doesn't cover the traffic). Run sudo fail2ban-client status sshd. If Total failed is 0 during attacks, fix the log source or filter. If the IP is in Banned IP list but still connects, check the jail's port, the banaction and Docker or proxy routing.

Quick diagnosis: not banning, or banned but not blocked?#

Start with fail2ban's own view of the jail:

bash
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client banned 203.0.113.42
output
$ sudo fail2ban-client banned 203.0.113.42
[['sshd']]

If the attacker's address is not banned, read Part 1. If it is banned in at least one jail but still connects, skip to Part 2.

For Part 1, test the filter against the real log source. For Part 2, compare the firewall rule with how the traffic arrives:

bash
# Part 1: does the filter match?
sudo fail2ban-regex /var/log/auth.log sshd       # file-based jails
sudo fail2ban-regex systemd-journal sshd         # journal-based jails

# Part 2: is the ban in the firewall, and how does the traffic arrive?
sudo nft list ruleset | grep -B2 -A8 f2b
sudo iptables -S | grep -i f2b
sudo ufw status numbered | grep 203.0.113.42
sudo ss -tnp | grep 203.0.113.42
What you see Most likely cause
Jail missing from Jail list Jail not enabled or not loaded
Total failed: 0, log file exists Filter doesn't match or wrong log file
Total failed: 0, Journal matches: shown Wrong journalmatch
Total failed rising, Currently failed stays low maxretry/findtime too lax
Ignore … by … lines in /var/log/fail2ban.log ignoreip or ignoreself
Found a match but no valid date/time warning Timestamp problem
Banned in fail2ban, address absent from the firewall banaction doesn't match your firewall, or the action errors
Rule present, but for port 22 while SSH listens on 2222 Wrong port
Rule present, ss shows the session still open Established connection
Target is a Docker container Docker bypasses INPUT
ss shows a Cloudflare or proxy address, not the banned one Proxy in front
Attacker uses an IPv6 address IPv6 not covered

Part 1: the jail doesn't ban#

In this case the jail usually isn't seeing the failures at all. It reads the wrong log file, the service logs to the systemd journal instead, or the filter doesn't fit your log format.

The jail isn't enabled#

Every jail in jail.conf ships with enabled = false. Only the jails you switch on in jail.local or jail.d/*.local run. On Debian and Ubuntu the package enables sshd for you in /etc/fail2ban/jail.d/defaults-debian.conf. On Fedora, RHEL, Rocky and AlmaLinux (EPEL) nothing is enabled until you do it.

Symptom: sudo fail2ban-client status doesn't list the jail, and sudo fail2ban-client status nginx-http-auth answers Sorry but the jail 'nginx-http-auth' does not exist.

Fix: enable the jail in /etc/fail2ban/jail.local. The section name must be exactly the jail name, in square brackets:

/etc/fail2ban/jail.local
[sshd]
enabled = true

[nginx-http-auth]
enabled = true
bash
sudo fail2ban-client -t && sudo fail2ban-client reload

Common reasons the jail still doesn't appear: the file is named jail.local.txt or lives somewhere other than /etc/fail2ban/, the section header has a typo ([ssh] instead of [sshd]), or a file loaded later sets enabled = false again. fail2ban reads jail.conf, then jail.d/*.conf, then jail.local, then jail.d/*.local, and the last value wins. sudo fail2ban-client -d | grep "'sshd'" shows what was actually loaded. A ready-made file is on the jail.local example page.

fail2ban is reading the wrong log#

A file-based jail only sees what's written to its logpath. If the service writes somewhere else, fail2ban watches an empty or stale file and never counts a failure.

Symptoms: File list: in the jail status points to a file that doesn't grow when you fail a login. Or the service refuses to start with Have not found any log file for sshd jail. That one has its own page.

bash
sudo fail2ban-client get sshd logpath
sudo tail -n 3 /var/log/auth.log        # is this file actually being written?

Typical cases:

  • Debian 12 and other journal-only systems. Debian 12 doesn't install rsyslog, so /var/log/auth.log doesn't exist and SSH failures only go to the systemd journal. Set backend = systemd for the jail (and install python3-systemd). Debian 13 and Ubuntu 24.04+ packages already do this for sshd.
  • Ubuntu 24.04: backend = systemd for every jail. Ubuntu 24.04's /etc/fail2ban/jail.d/defaults-debian.conf sets backend = systemd under [DEFAULT]. Every jail you enable then reads the journal and ignores its logpath. That's fine for sshd, but nginx, Apache and most app jails log to files and never count a failure. Add backend = auto to each file-based jail. The jail status should then show File list: instead of Journal matches:.
  • RHEL-family systems log SSH to /var/log/secure, not auth.log. The packaged paths-fedora.conf handles that, unless you hard-coded logpath = /var/log/auth.log from a Debian tutorial.
  • Web servers with per-site logs. Nginx and Apache virtual hosts often log to /var/log/nginx/example.com.error.log. Point logpath at those files; globs like /var/log/nginx/*error.log work.
  • Apps in containers log to stdout. fail2ban on the host can't see that unless the log is bind-mounted to a file or read from the journal (journald log driver). The Docker guide shows both.

The backend page explains auto, pyinotify, polling and systemd in detail.

The filter doesn't match your log lines#

If the right file is being read but Total failed stays at 0, the filter's regex doesn't fit your log format. fail2ban-regex tells you for certain:

bash
sudo fail2ban-regex /var/log/auth.log sshd
sudo fail2ban-regex /var/log/nginx/error.log nginx-http-auth

Look at the Failregex: total and the Lines: summary at the end. If matched is 0 but the file contains failed logins, the filter is wrong for your format. Typical causes:

  • Custom log format. A changed nginx log_format, an app that logs JSON, or a syslog template that prepends extra fields. Write a filter.d/<name>.local with an adjusted failregex. The custom filter guide and the regex tester help.
  • OpenSSH 9.8+ and an older filter. Since OpenSSH 9.8 many messages come from a process called sshd-session instead of sshd. The upstream sshd filter matches both only from fail2ban 1.1.1. Debian 13 patched its 1.1.0 package. A hand-installed 1.1.0 or older needs an update.
  • SSH with keys only. In the default mode = normal, bots that connect and disconnect without trying a password aren't counted as failures. If your log is full of Connection closed by authenticating user … [preauth], set mode = aggressive for the jail. The SSH guide covers all filter modes.

Wrong journalmatch for SSH#

With backend = systemd the jail doesn't read a file. It reads journal entries that match journalmatch. The upstream sshd filter uses _SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session (the + means OR). On Debian and Ubuntu the SSH unit is called ssh.service, so a hand-written journalmatch = _SYSTEMD_UNIT=sshd.service matches nothing.

bash
systemctl status ssh sshd 2>/dev/null | grep -m2 Loaded
sudo fail2ban-client get sshd journalmatch
sudo journalctl _SYSTEMD_UNIT=ssh.service -n 5

Fix: remove your custom journalmatch so the filter default applies, or set it to match your unit:

/etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session

ignoreip or ignoreself covers the address#

fail2ban never bans addresses in ignoreip, and with ignoreself = true (the default) it never bans the server's own addresses. Testing from the server itself, from another container on the same host, or from a whitelisted LAN produces no ban, by design.

Symptom: lines like [sshd] Ignore 192.168.1.20 by ip in /var/log/fail2ban.log.

bash
sudo fail2ban-client get sshd ignoreip
sudo fail2ban-client get sshd ignoreself
sudo grep "Ignore " /var/log/fail2ban.log | tail

Test from a different network, such as a phone on mobile data. If the list contains something broad like 10.0.0.0/8 or a dynamic DNS name, narrow it. More in whitelisting with ignoreip.

maxretry and findtime are too lax#

The defaults are maxretry = 5 within findtime = 10m. A slow bot that tries once every three minutes never reaches 5 failures in 10 minutes, so it's never banned. You'll see Total failed climbing while Currently failed hovers at 1 or 2.

Fix: widen the window and lower the threshold for internet-facing services:

/etc/fail2ban/jail.local
[sshd]
enabled  = true
maxretry = 3
findtime = 1h
bantime  = 1h

Check what's actually in effect with sudo fail2ban-client get sshd maxretry and sudo fail2ban-client get sshd findtime (seconds). For persistent slow attackers, add the recidive jail. The trade-offs are explained on bantime, findtime and maxretry.

Timestamps or time zone don't line up#

fail2ban only counts a failure if it can read the time from the log line and that time falls inside findtime. Two warnings in /var/log/fail2ban.log point here:

  • Found a match but no valid date/time found for '…' means the line matched but fail2ban couldn't parse its date. Set a datepattern for the jail (see man jail.conf) or fix the application's log format.
  • Detected a log entry 2h before the current time in operation mode. This looks like a timezone problem. means the log is written in a different time zone than the system. Set logtimezone = UTC (or the right offset) in the jail, or fix the app's time zone. Containers that log in UTC on a host set to local time hit this often.

Lines older than findtime are skipped when fail2ban starts and reads a log from the beginning. That's why replaying an old log file doesn't produce bans.

The log shows the proxy's IP, not the attacker's#

If your web app sits behind nginx, Traefik, Caddy, a load balancer or Cloudflare, its log may record the proxy's address for every request. fail2ban then either bans the proxy (if it isn't ignored) or nothing (if it is, for example 127.0.0.1 covered by ignoreself). Fix the logging first: fail2ban bans the proxy IP.

Host names instead of IPs (usedns)#

Some services log a host name instead of an IP. With the default usedns = warn, fail2ban resolves it and logs a warning. With usedns = no those lines are ignored entirely. Prefer configuring the service to log IP addresses. For SSH, keep UseDNS no (the OpenSSH default) in sshd_config.

Part 2: the IP is banned but still connects#

fail2ban doesn't block anything itself. It runs the commands of the jail's ban action, which add a firewall rule. If the IP is banned in fail2ban but still gets in, that rule doesn't apply to the traffic you care about.

The jail uses the wrong port#

The ban rule only applies to the ports set in the jail. The sshd jail uses port = ssh, which is port 22. If you moved SSH to 2222, the rule blocks 22 and the attacker keeps hitting 2222.

Symptom: the nftables rule reads tcp dport 22 (or ssh) and the iptables rule shows --dport 22, but ss -tlnp shows sshd listening on a different port.

Fix: set the real port(s) in the jail. Several ports are comma-separated:

/etc/fail2ban/jail.local
[sshd]
enabled = true
port    = 2222

[nginx-http-auth]
enabled = true
port    = http,https,8443
bash
sudo fail2ban-client reload sshd

Reloading the jail re-creates the rule with the new port. For services where any open port matters, ban all ports instead: banaction = %(banaction_allports)s in the jail.

The banaction doesn't match your firewall#

If the action's commands target a firewall that isn't the one filtering your traffic, or fail outright, the ban exists only in fail2ban's memory. Check which action the jail uses and look for action errors:

output
$ sudo fail2ban-client get sshd actions
The jail sshd has the following actions:
nftables
bash
sudo grep -E "ERROR|returned [0-9]+|not found" /var/log/fail2ban.log | tail -20

Common mismatches:

  • iptables action on an nftables-only system. Debian 12's fail2ban 1.0.2 defaults to iptables-multiport. A minimal install without the iptables package logs iptables: not found and the action returns 127. Install iptables, or switch to banaction = nftables and banaction_allports = nftables[type=allports]. See config errors.
  • firewalld manages the firewall. Use the firewallcmd-* actions such as firewallcmd-ipset or firewallcmd-rich-rules, so bans live inside firewalld's own rules and survive firewall-cmd --reload. Fedora's fail2ban-firewalld package sets this up. See the firewalld guide.
  • iptables-nft vs iptables-legacy. Both backends' rules are evaluated by the kernel, so a ban in either one still drops packets. But iptables -S only shows rules from the backend it's linked to. Check iptables -V (it prints nf_tables or legacy) and look with the matching tool before you conclude the rule is missing.
  • A firewall restart wiped the rules. ufw reload, a restart of nftables.service (Debian's /etc/nftables.conf starts with flush ruleset) or a custom firewall script removes fail2ban's rules. The upstream fail2ban.service is PartOf= the iptables, firewalld and nftables services, so fail2ban restarts with them and re-applies its bans. Custom scripts don't get that, so restart fail2ban after running them.

The connection was already open#

Whether a ban kills an open session depends on the action:

  • nftables and iptables (fail2ban's chain is inserted at the top of INPUT) match every packet from the address, so established sessions stall too.
  • ufw inserts its rule into ufw's user chains, which come after the "accept established connections" rule. Only new connections are blocked, so an attacker already connected keeps going. The ufw action's kill-mode option fixes this:
/etc/fail2ban/jail.local
[DEFAULT]
banaction = ufw[kill-mode=ss]

kill-mode=ss runs ss -K dst <ip> to kill the sockets. kill-mode=conntrack runs conntrack -D -s <ip> and needs the conntrack package. firewalld rich rules behave like ufw for established connections. The ufw guide covers the rest of the ufw setup.

Docker containers bypass the INPUT chain#

Traffic to a published Docker port is DNAT-ed and goes through the FORWARD path, not INPUT. Rules in INPUT or in nftables chains hooked on input never see it. Use chain = DOCKER-USER with an iptables action, or an nftables chain on the forward hook. The full walk-through is in the fail2ban Docker guide.

A proxy or CDN sits in front#

If Cloudflare, a load balancer or a reverse proxy on another host terminates the connection, packets reaching your server come from the proxy's address. Banning the visitor's real IP in your local firewall does nothing, because that IP never connects to you directly. Ban at the edge instead (the cloudflare-token action, see the Cloudflare guide) or in the proxy. See also fail2ban bans the proxy IP.

IPv6 isn't covered#

fail2ban bans IPv6 addresses when the ban action supports them. nftables and iptables (via ip6tables) do. Problems appear when:

  • ip6tables isn't installed, so IPv6 bans fail with an action error.
  • ufw has IPV6=no in /etc/default/ufw.
  • A custom action only handles IPv4.

Check for IPv6 entries with sudo fail2ban-client get sshd banip, and make sure the action shows an IPv6 set (addr6-set-sshd for nftables) or ip6tables rule after an IPv6 ban.

An ACCEPT rule comes before fail2ban's rule#

In iptables, the first matching rule wins. fail2ban inserts its jump at position 1 of the chain when the action starts, which is at the first ban. Rules inserted later with -I, such as a script that adds -I INPUT -p tcp --dport 22 -j ACCEPT or a firewall that rebuilds the chain, can end up above it. sudo iptables -L INPUT -n --line-numbers shows the order. The f2b-sshd jump must come before any ACCEPT for the same port.

In nftables this can't happen across tables. fail2ban's chain runs at priority -1 on the input hook, and its reject is final no matter what other chains accept later.

Confirm the fix#

For a Part 1 fix, trigger failures from an address that isn't ignored and watch fail2ban react:

bash
sudo tail -f /var/log/fail2ban.log
log
2026-10-25 18:36:10,238 fail2ban.filter         [1997]: INFO    [sshd] Found 203.0.113.42 - 2026-10-25 18:36:10
2026-10-25 18:36:11,027 fail2ban.actions        [1997]: NOTICE  [sshd] Ban 203.0.113.42

Found lines prove the log source and filter work. A Ban line proves the thresholds are reached.

For a Part 2 fix, ban a test address you control, such as a VPS or a phone hotspot, check the rule and try to connect:

bash
sudo fail2ban-client set sshd banip 198.51.100.7
sudo nft list set inet f2b-table addr-set-sshd    # nftables
sudo iptables -L f2b-sshd -n                      # iptables

The connection attempt should time out or be refused. Afterwards lift the test ban with sudo fail2ban-client unban 198.51.100.7 (more unban options).

FAQ#

Why is fail2ban not banning SSH attempts?#

Usually the sshd jail reads /var/log/auth.log on a system that logs only to the systemd journal, or the filter doesn't count key-only probes. Set backend = systemd for [sshd] if auth.log doesn't exist, and use mode = aggressive to count pre-auth disconnects. Check Total failed in fail2ban-client status sshd.

Why does fail2ban show failures but never ban?#

Failures are counted per IP within findtime. If an IP doesn't reach maxretry failures inside that window, it's never banned. Slow bots exploit this. Raise findtime to an hour or more and lower maxretry. Also check the IP isn't covered by ignoreip or ignoreself.

Why does fail2ban ban an IP but it can still connect?#

The firewall rule doesn't apply to that traffic. The jail's port may not match the real service port, the banaction may target a firewall that isn't filtering your traffic, the traffic may go to Docker through FORWARD, or a proxy or CDN may connect on the visitor's behalf. Compare the firewall rule with ss -tn output.

Why is fail2ban not creating iptables rules?#

fail2ban creates its iptables chain only at the first ban, so an empty ruleset after startup is normal. If rules still don't appear after a ban, the jail probably uses an nftables or ufw action, or the iptables command failed. Check fail2ban-client get <jail> actions and grep /var/log/fail2ban.log for ERROR.

Does fail2ban close existing connections?#

With the nftables and iptables actions, yes in practice: every packet from the banned address is rejected, so open sessions stall. With ufw and firewalld rich rules, established connections are accepted before the ban rule. Use banaction = ufw[kill-mode=ss] to kill them at ban time.

How do I test whether fail2ban is working?#

Fail a few logins from an outside address, such as a phone on mobile data, and watch /var/log/fail2ban.log for Found and Ban lines. To test the firewall side, ban a test address by hand with sudo fail2ban-client set sshd banip 192.0.2.15, check the rule, then unban it.

Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.