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.
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:
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client banned 203.0.113.42$ 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:
# 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:
[sshd]
enabled = true
[nginx-http-auth]
enabled = truesudo fail2ban-client -t && sudo fail2ban-client reloadCommon 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.
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.logdoesn't exist and SSH failures only go to the systemd journal. Setbackend = systemdfor the jail (and installpython3-systemd). Debian 13 and Ubuntu 24.04+ packages already do this forsshd. - Ubuntu 24.04:
backend = systemdfor every jail. Ubuntu 24.04's/etc/fail2ban/jail.d/defaults-debian.confsetsbackend = systemdunder[DEFAULT]. Every jail you enable then reads the journal and ignores itslogpath. That's fine forsshd, but nginx, Apache and most app jails log to files and never count a failure. Addbackend = autoto each file-based jail. The jail status should then showFile list:instead ofJournal matches:. - RHEL-family systems log SSH to
/var/log/secure, notauth.log. The packagedpaths-fedora.confhandles that, unless you hard-codedlogpath = /var/log/auth.logfrom a Debian tutorial. - Web servers with per-site logs. Nginx and Apache virtual hosts often log to
/var/log/nginx/example.com.error.log. Pointlogpathat those files; globs like/var/log/nginx/*error.logwork. - 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:
sudo fail2ban-regex /var/log/auth.log sshd
sudo fail2ban-regex /var/log/nginx/error.log nginx-http-authLook 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 afilter.d/<name>.localwith an adjustedfailregex. 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-sessioninstead ofsshd. The upstreamsshdfilter 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 ofConnection closed by authenticating user … [preauth], setmode = aggressivefor 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.
systemctl status ssh sshd 2>/dev/null | grep -m2 Loaded
sudo fail2ban-client get sshd journalmatch
sudo journalctl _SYSTEMD_UNIT=ssh.service -n 5Fix: remove your custom journalmatch so the filter default applies, or set it to match your unit:
[sshd]
enabled = true
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-sessionignoreip 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.
sudo fail2ban-client get sshd ignoreip
sudo fail2ban-client get sshd ignoreself
sudo grep "Ignore " /var/log/fail2ban.log | tailTest 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:
[sshd]
enabled = true
maxretry = 3
findtime = 1h
bantime = 1hCheck 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 adatepatternfor the jail (seeman 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. Setlogtimezone = 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:
[sshd]
enabled = true
port = 2222
[nginx-http-auth]
enabled = true
port = http,https,8443sudo fail2ban-client reload sshdReloading 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:
$ sudo fail2ban-client get sshd actions
The jail sshd has the following actions:
nftablessudo grep -E "ERROR|returned [0-9]+|not found" /var/log/fail2ban.log | tail -20Common mismatches:
- iptables action on an nftables-only system. Debian 12's fail2ban 1.0.2 defaults to
iptables-multiport. A minimal install without theiptablespackage logsiptables: not foundand the action returns 127. Installiptables, or switch tobanaction = nftablesandbanaction_allports = nftables[type=allports]. See config errors. - firewalld manages the firewall. Use the
firewallcmd-*actions such asfirewallcmd-ipsetorfirewallcmd-rich-rules, so bans live inside firewalld's own rules and survivefirewall-cmd --reload. Fedora'sfail2ban-firewalldpackage 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 -Sonly shows rules from the backend it's linked to. Checkiptables -V(it printsnf_tablesorlegacy) and look with the matching tool before you conclude the rule is missing. - A firewall restart wiped the rules.
ufw reload, a restart ofnftables.service(Debian's/etc/nftables.confstarts withflush ruleset) or a custom firewall script removes fail2ban's rules. The upstreamfail2ban.serviceisPartOf=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-modeoption fixes this:
[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:
ip6tablesisn't installed, so IPv6 bans fail with an action error.- ufw has
IPV6=noin/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:
sudo tail -f /var/log/fail2ban.log2026-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.42Found 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:
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 # iptablesThe 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.