Guides

fail2ban on Ubuntu and Debian: jail.local, the SSH jail, unbanning and nginx jails

Install fail2ban, put ignoreip and the sshd jail into jail.local (backend = systemd on Debian 12), check with fail2ban-client status and unban with unbanip.

Published ~10 min read

Short answer: install fail2ban with sudo apt install fail2ban nftables python3-systemd, leave jail.conf alone and put your settings into /etc/fail2ban/jail.local: your own IP address in ignoreip first, then the sshd jail. On Debian 12 that jail also needs backend = systemd, or fail2ban refuses to start. fail2ban-client status sshd shows what it banned, and fail2ban-client set sshd unbanip IP lifts a ban.

Debian · Ubuntu
sudo apt update
sudo apt install -y fail2ban nftables python3-systemd
sudo nano /etc/fail2ban/jail.local

The complete jail.local is below; the minimum is the [DEFAULT] line with your IP and the [sshd] section. Then test and apply:

Debian · Ubuntu
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

-t reads the whole configuration and prints OK: configuration test is successful, or the line that is wrong, without touching the running service. The configuration files here pass that test with fail2ban 1.0.2 on Debian 12, the filters were run through fail2ban-regex, and the defaults were checked against the packages of Debian 13 and Ubuntu 24.04 and 26.04.

jail.conf, jail.local and jail.d: which file wins

fail2ban reads jail.conf, then jail.d/*.conf in alphabetical order, then jail.local, then jail.d/*.local; a value read later wins. jail.conf belongs to the package and is replaced on upgrades. A .local file only needs the keys you change, so do not copy the whole jail.conf into it, as many guides suggest: that freezes every default at today’s value. Two syntax rules catch people out:

  • A comment is a line that starts with #. After a value, only ; preceded by a space starts a comment; port = 2222 # ssh passes the whole text on as the port.
  • A section may appear once per file. A second [sshd] stops the configuration with section 'sshd' already exists.

sudo fail2ban-client -d prints the configuration as fail2ban resolved it, with every jail’s backend, log paths and ban commands, which settles any doubt about which file won.

What Debian and Ubuntu ship

The stock settings differ by release, and the differences explain most “fail2ban does not start” and “fail2ban never bans” reports:

ReleaseVersionStock setupWhat you have to do
Debian 121.0.2sshd jail reads /var/log/auth.log, bans with iptablesThat file does not exist, because Debian 12 logs to the journal only, so fail2ban stops with Have not found any log file for sshd jail. Set backend = systemd and an nftables ban action
Debian 131.1.0sshd jail reads the journal, bans with nftablesInstall the nftables package if nft is missing
Ubuntu 24.041.0.2Every jail reads the journal (backend = systemd in [DEFAULT]), bans with nftablesJails for log files, such as nginx and recidive, need backend = auto
Ubuntu 26.041.1.0sshd jail reads the journal, bans with nftablesNothing for SSH

The nftables actions run the nft command from the nftables package. Debian’s cloud images of 12 and 13 ship only iptables, and fail2ban merely recommends “nftables or iptables”, so apt does not add it: hence nftables in the install line. Installing it enables no rules of its own; its service stays off. The journal backend needs python3-systemd, which Debian 12’s package also only recommends, so it is named too; newer packages depend on it anyway. On Ubuntu 24.04, the fail2ban package first shipped with the release crashed on Python 3.12 with No module named 'asynchat'; version 1.0.2-3ubuntu0.1 fixed it, which is why apt update comes first.

A jail.local that works on Debian and Ubuntu

This passes fail2ban-client -t on Debian 12 and does the right thing on the newer releases, where it repeats what the package already sets:

/etc/fail2ban/jail.local
[DEFAULT]
# Never ban these. Put your own fixed IP or range here FIRST.
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 198.51.100.0/24

bantime  = 1h
findtime = 10m
maxretry = 5

# Repeat offenders: 1h, 2h, 4h ... capped at one week
bantime.increment = true
bantime.maxtime   = 1w

# Ban through nftables (the Debian 12 default is iptables)
banaction          = nftables
banaction_allports = nftables[type=allports]

[sshd]
enabled = true
# Read sshd from the journal: Debian 12 has no /var/log/auth.log
backend = systemd
  • ignoreip takes addresses and CIDR ranges, IPv4 and IPv6, separated by spaces. The server’s own addresses are ignored anyway (ignoreself is on by default). A jail that sets its own ignoreip replaces this list, so repeat your address there.
  • maxretry, findtime, bantime: 5 failures from one address within 10 minutes mean a ban for one hour. The stock values are 5, 10 minutes and 10 minutes. Times take m, h, d and w; bantime = -1 bans for good, which on a busy server only grows the ban list forever.
  • bantime.increment doubles the ban each time the same address is banned again, using the history in /var/lib/fail2ban/fail2ban.sqlite3, so it survives restarts. bantime.maxtime caps it. The log then shows Increase Ban lines.
  • banaction = nftables creates its own table inet f2b-table with a chain that runs before the rules UFW manages, and an address set per jail and IP version. Banned clients are rejected, so most see Connection refused; some, Windows among them, wait for a timeout instead. Because the bans live in that table, sudo ufw status never lists them; sudo nft list table inet f2b-table does. If you would rather see bans in UFW, banaction = ufw adds each one as a UFW rule, which blocks the address on every port.
  • backend = systemd in [sshd] only. Do not put it into [DEFAULT]: jails for nginx and recidive read log files, and with the journal backend fail2ban ignores their logpath, searches the journal, where those logs are not, and never matches, without a warning.

If SSH listens on another port, add port = 2222 to [sshd], or fail2ban bans attackers on 22 while they knock on 2222; the SSH port change guide has the details.

Normal or aggressive mode for SSH

The sshd filter has modes. normal counts failed logins and invalid users; aggressive also counts connections that are dropped before or during authentication, such as scanners that never try a login and clients offering no accepted key or no common cipher. Add mode = aggressive to [sshd]. On a two-hour sample of SSH log from an internet-facing server, normal mode matched 63 of 193 lines and aggressive mode 107.

Check that it bans

Debian · Ubuntu
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd ignoreip
sudo tail -f /var/log/fail2ban.log
Example output · fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	2
|  |- Total failed:	57
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned:	2
   |- Total banned:	4
   `- Banned IP list:	192.0.2.77 192.0.2.140

Total failed must grow over time on any server reachable from the internet. A jail that reads log files shows File list: instead of Journal matches:. The log records each step:

Example output · /var/log/fail2ban.log
2026-11-04 09:12:44,071 fail2ban.filter         [812]: INFO    [sshd] Found 192.0.2.77 - 2026-11-04 09:12:44
2026-11-04 09:12:52,388 fail2ban.actions        [812]: NOTICE  [sshd] Ban 192.0.2.77

Unban an IP address

Debian · Ubuntu
# Which jails hold this address? Prints [[]] if none
sudo fail2ban-client banned 203.0.113.10

# Unban from one jail: prints 1 if it was banned, 0 if not
sudo fail2ban-client set sshd unbanip 203.0.113.10

# Unban from every jail at once
sudo fail2ban-client unban 203.0.113.10

An unban also clears the address from the database, so it starts from zero. If you locked yourself out, run the unban from the console, add the address to ignoreip, and load the change with sudo fail2ban-client reload. The opposite, sudo fail2ban-client set sshd banip 192.0.2.77, bans an address by hand.

Test a filter with fail2ban-regex

fail2ban-regex runs a filter over a log line or file and reports what it would count, without banning anything. It reads the journal too, but over a large journal that is slow; exporting a recent window is quicker:

Debian · Ubuntu
sudo journalctl -u ssh --since "-2h" -o short > /tmp/ssh.log
fail2ban-regex /tmp/ssh.log sshd
fail2ban-regex /tmp/ssh.log 'sshd[mode=aggressive]'

# One line against a filter, e.g. before enabling an nginx jail
fail2ban-regex '2026/11/04 09:15:02 [error] 4242#4242: *1830 limiting requests, excess: 20.840 by zone "perip", client: 192.0.2.77, server: example.com, request: "GET /login HTTP/1.1", host: "example.com"' nginx-limit-req

The result ends with one summary line; this is the real one from the sample above:

Example output · fail2ban-regex /tmp/ssh.log sshd
Lines: 193 lines, 104 ignored, 63 matched, 26 missed

matched lines count towards maxretry. ignored lines matched a helper pattern that records the address without counting a failure, such as a closed connection. missed lines matched no rule, which is normal for successful logins and session messages; add --print-all-missed to read them when you expected a match. Zero matched lines on a log full of failures means the wrong filter, log or date format.

Recidive: long bans for repeat offenders

The recidive jail reads fail2ban’s own log and bans addresses that other jails banned 5 times within a day, for a week and on all ports. It overlaps with bantime.increment, but works across jails. It reads a file, so give it backend = auto:

/etc/fail2ban/jail.d/recidive.local
[recidive]
enabled = true
backend = auto

An email for each ban

fail2ban can mail you on a ban, but it sends mail through /usr/sbin/sendmail, so the server needs a mail transfer agent that can deliver or relay to your mailbox. Do not turn it on for sshd, which bans around the clock; on the recidive jail it reports only the persistent cases. Add two lines to the section:

/etc/fail2ban/jail.d/recidive.local
[recidive]
enabled   = true
backend   = auto
destemail = admin@example.com
action    = %(action_mw)s

action_mw bans as before and mails a report with the whois data of the address; action_mwl adds the matching log lines. The jail also mails when it starts and stops, so every restart of fail2ban sends one too.

nginx jails: limit-req, botsearch and http-auth

fail2ban ships filters for nginx’s error log (/var/log/nginx/*error.log by default):

  • nginx-limit-req bans clients that keep exceeding an nginx rate limit. It needs limit_req configured, or nothing is ever logged.
  • nginx-botsearch bans clients that probe typical attack paths that do not exist on your server, such as wp-login.php, phpMyAdmin or webmail. It reads nginx’s No such file or directory errors, so it only sees paths nginx tries to serve from disk; requests that proxy_pass hands to an app never show up there.
  • nginx-http-auth bans wrong passwords for HTTP basic auth. nginx-bad-request reads the access log and bans clients that cause 400 responses.
nginx · rate limit for nginx-limit-req (tested with nginx -t)
# in the http { } block, e.g. /etc/nginx/conf.d/ratelimit.conf
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;

# in the location you want to protect, e.g. location /login { ... }
limit_req zone=perip burst=20 nodelay;

Put limit_req on login, search or API paths rather than the whole server: one page that loads a few dozen assets at once can exceed the burst, and each rejected request is a log line the jail counts.

/etc/fail2ban/jail.d/nginx.local
[nginx-limit-req]
enabled  = true
backend  = auto
maxretry = 10

[nginx-botsearch]
enabled  = true
backend  = auto
maxretry = 2
bantime  = 1d

backend = auto is the line people miss on Ubuntu 24.04: without it these jails inherit the journal backend, start without a warning and never ban. Behind Cloudflare, nginx logs Cloudflare’s addresses unless you restore the visitor’s IP, and a web jail then bans Cloudflare instead of the attacker. Fix that before you enable any web jail, as shown under Cloudflare error 522.

If nginx runs in a Docker container, its traffic is forwarded, not delivered locally, and a ban in the input chain never sees it. For such a jail use an iptables action in Docker’s own chain: banaction = iptables-multiport plus chain = DOCKER-USER. The jail also needs the container’s log as a file on the host, for example through a mounted log directory, because nginx in a container logs to Docker by default.

When fail2ban does not start or does not ban

SymptomCauseFix
Failed during configuration: Have not found any log file for sshd jailNo /var/log/auth.log (Debian 12), or a logpath that does not existbackend = systemd in [sshd], or correct the path
No module named 'asynchat'Ubuntu 24.04 with the fail2ban version from the release imagesudo apt update && sudo apt install --only-upgrade fail2ban
section 'sshd' already existsThe same section twice in one fileMerge the two sections
Jail runs, Total failed stays 0Wrong backend, a web jail on the journal, or the filter does not match your logCheck with fail2ban-client -d and fail2ban-regex
Address banned, attacks still arriveSSH on another port, Docker-published ports, or traffic through CloudflareSet port, use DOCKER-USER, restore real IPs
You banned yourselfYour IP is not in ignoreipUnban from the console, then add it

fail2ban is one layer. Key-only logins with root login off, from the SSH hardening guide, stop password guessing on their own, and a UFW policy that only opens the ports you serve leaves fail2ban less to watch. Which port sshd really uses, and how to test it from outside, is in SSH port 22.

Frequently asked

How do I unban an IP address in fail2ban?

Run 'sudo fail2ban-client set sshd unbanip 203.0.113.10' to unban it from one jail; it prints 1 if the address was banned and 0 if not. 'sudo fail2ban-client unban 203.0.113.10' unbans it from every jail, and 'sudo fail2ban-client banned 203.0.113.10' tells you first which jails hold it. Add the address to ignoreip in jail.local and run 'sudo fail2ban-client reload' so it does not happen again.

Why does fail2ban not start on Debian 12?

Debian 12 no longer writes /var/log/auth.log, but its fail2ban package still points the sshd jail there, so it stops with 'Have not found any log file for sshd jail'. Add backend = systemd to the [sshd] section of /etc/fail2ban/jail.local, check with 'sudo fail2ban-client -t' and restart fail2ban. Debian 13 and Ubuntu read the journal by default.

Should I edit jail.conf or jail.local?

jail.local. jail.conf belongs to the package and is replaced on upgrades. jail.local and .local files in jail.d/ are read after it and only need the settings you change. Copying all of jail.conf into jail.local, as older guides suggest, freezes every default at the old value.

Related articles

All guides