Configuration
fail2ban jail.local example (annotated)
Copy a working /etc/fail2ban/jail.local, understand every line, and adapt it to your distro. Includes a five-line minimal version and how to add your own jails.
jail.local is your override file for fail2ban jails. Create /etc/fail2ban/jail.local, set global defaults under [DEFAULT] (bantime, findtime, maxretry, ignoreip), then enable each jail with enabled = true in its own section, for example [sshd]. You only list what you change. Everything else still comes from jail.conf. Apply it with sudo fail2ban-client reload.
Minimal jail.local#
If you only want SSH protection with sane values, this is enough:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
bantime = 1h
[sshd]
enabled = truesudo fail2ban-client -t && sudo fail2ban-client reload
sudo fail2ban-client status sshdReplace 192.168.1.0/24 with your own LAN, and add your admin IP if you connect from a fixed address. The whitelist guide covers dynamic IPs.
Complete jail.local example#
This is a production-ready starting point for a typical VPS running SSH and nginx. Each block is explained below.
#
# /etc/fail2ban/jail.local — local overrides, survives package upgrades
#
[DEFAULT]
# Never ban these: loopback, your LAN, your office/VPN address.
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 203.0.113.10
# Ban for 1 hour after 5 failures within 10 minutes.
bantime = 1h
findtime = 10m
maxretry = 5
# Repeat offenders: each new ban doubles (1h, 2h, 4h, 8h…), capped at 1 week.
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 1w
# Read log files with pyinotify/polling unless a jail says otherwise.
backend = auto
# Firewall used to block. Delete these two lines on firewalld systems
# (Rocky/Alma/RHEL/Fedora), where the package already sets firewalld actions.
banaction = nftables
banaction_allports = nftables[type=allports]
# Ban only (no email). For email: set destemail/sender and action = %(action_mw)s
action = %(action_)s
[sshd]
enabled = true
port = ssh ; use the real port if sshd listens elsewhere, e.g. 2222
maxretry = 3
# Read from the systemd journal (Debian 12+, Ubuntu 24.04+, RHEL 9+).
# Needs python3-systemd. Use "backend = auto" if /var/log/auth.log or /var/log/secure exists.
backend = systemd
[recidive]
# Bans IPs that keep getting banned by other jails, on all ports.
enabled = true
bantime = 1w
findtime = 1d
maxretry = 5
bantime.maxtime = 5w ; let repeat recidive bans grow beyond the 1w default cap
[nginx-http-auth]
# Failed HTTP basic auth logins (nginx error log).
enabled = true
port = http,https
[nginx-botsearch]
# Requests for /wp-login.php, /phpmyadmin, etc. that return "not found".
enabled = true
port = http,https
maxretry = 2
[nginx-bad-request]
# 400 Bad Request responses in the access log (scanners, malformed requests).
enabled = true
port = http,httpsPrefer a form? The jail.local generator builds this file from checkboxes for your services and distro.
Line-by-line explanation#
[DEFAULT]: settings every jail inherits#
| Line | What it does |
|---|---|
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 203.0.113.10 |
Addresses fail2ban never bans. Space-separated IPs, CIDR ranges or hostnames. Your own server's addresses are already covered by ignoreself = true. |
bantime = 1h |
How long a ban lasts. The stock default is 10m, which a botnet barely notices. |
findtime = 10m |
The window in which failures are counted. |
maxretry = 5 |
Failures within findtime that trigger a ban. How the three interact is explained in bantime, findtime and maxretry. |
bantime.increment = true |
Each new ban of the same IP is longer. Needs the fail2ban database, which is on by default. See incremental bans. |
bantime.factor = 1 |
Multiplier for the growth formula. 1 doubles the ban each time. |
bantime.maxtime = 1w |
Upper limit for an increased ban. Without it, bans keep doubling for up to 20 repeat bans. |
backend = auto |
Watch log files (pyinotify if available, otherwise polling). Setting it here also overrides distro files that make systemd the global default. |
banaction / banaction_allports |
The firewall action used by every jail: per-port bans and all-port bans. See banaction. |
action = %(action_)s |
Ban only. %(action_mw)s also emails a whois report and %(action_mwl)s adds the log lines (email alerts). |
[sshd]: SSH brute force#
The section name [sshd] picks the stock jail from jail.conf and its filter filter.d/sshd.conf. You only set what differs:
enabled = true: every stock jail is disabled until you do this.port = ssh: the ports to block.sshresolves to 22 through/etc/services. If you moved SSH, write the number (port = 2222), otherwise the ban blocks port 22 while the attacker keeps hitting 2222.maxretry = 3: stricter than the default for a service where real users rarely fail three times.backend = systemd: reads sshd's messages from the journal using the filter'sjournalmatch. Required on systems without rsyslog (Debian 12 has no/var/log/auth.logby default). With this backend anylogpathis ignored.
For more aggressive matching (pre-auth disconnects, scanners that never try a password) add mode = aggressive. Details on the sshd filter page and in the SSH guide.
[recidive]: long bans for repeat offenders#
The stock recidive jail reads fail2ban's own log (/var/log/fail2ban.log) and bans any IP that was banned 5 times within a day, on all ports (banaction_allports), for a week. The bantime.maxtime = 5w line is there because the [DEFAULT] cap of 1w would otherwise stop a repeat recidive ban from growing.
If fail2ban logs to the systemd journal instead of a file, recidive needs backend = systemd. The recidive page explains how to check.
nginx jails#
The nginx jails need no logpath: jail.conf already points them at /var/log/nginx/*error.log or *access.log (from paths-common.conf). Add logpath = only if your logs live elsewhere, for example per-site logs:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/*error.log
/srv/www/*/logs/error.logBehind Cloudflare or another proxy, nginx must log the real client IP first, or fail2ban will ban the proxy. See the reverse proxy fix and the nginx guide.
Distro-specific notes#
Packages ship /etc/fail2ban/jail.d/defaults-debian.conf, which jail.local overrides:
- Ubuntu 24.04 (fail2ban 1.0.2):
banaction = nftables,banaction_allports = nftables[type=allports],backend = systemdin[DEFAULT], and[sshd] enabled = true. Because the global backend issystemd, file-based jails like nginx read nothing until you setbackend = auto, as the example does. - Debian 13 / Ubuntu 26.04 (1.1.0): nftables actions, plus
[sshd] backend = systemdwithjournalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd. - Debian 12 (1.0.2): only
[sshd] enabled = true. There is no rsyslog by default, so/var/log/auth.logis missing and sshd fails with "Have not found any log file". Fix:sudo apt install python3-systemdandbackend = systemdin[sshd].
cat /etc/fail2ban/jail.d/defaults-debian.conffail2ban comes from EPEL (1.1.0 on EL9/EL10). The fail2ban meta-package pulls in fail2ban-firewalld, which installs /etc/fail2ban/jail.d/00-firewalld.conf with firewalld actions. Delete the banaction lines from the example so they don't replace them, unless you've disabled firewalld.
SSH failures go to /var/log/secure (paths-fedora.conf), and journal reading needs python3-systemd. More in install on RHEL.
cat /etc/fail2ban/jail.d/00-firewalld.confSame layout as RHEL: fail2ban-firewalld adds firewalld actions and fail2ban-systemd adds jail.d/00-systemd.conf with backend = systemd. As on Ubuntu 24.04, keep backend = auto in [DEFAULT] for file-based jails and set backend = systemd on journal jails like [sshd].
paths-arch.conf already sets sshd_backend = systemd (and for postfix, dovecot, FTP servers), so [sshd] reads the journal without extra settings. Install python-systemd if it isn't pulled in. Full steps: install fail2ban on Arch.
Add a new jail#
Any section you add to jail.local becomes a jail. For a service that already has a filter in filter.d/, name the section after the filter:
[postfix-sasl]
enabled = trueFor your own application, write a filter filter.d/myapp.conf (see writing a custom filter) and point a custom jail at it:
[myapp]
enabled = true
filter = myapp
logpath = /var/log/myapp/auth.log
port = http,https
maxretry = 5
backend = autofilter = myapp is optional when the section and filter share a name, because the default is filter = %(__name__)s. Test the filter against the log before you enable the jail:
sudo fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.confUse jail.d drop-ins instead#
The same configuration can be split into one file per jail. fail2ban reads jail.d/*.local after jail.local, alphabetically:
[sshd]
enabled = true
maxretry = 3
backend = systemd[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = trueKeep [DEFAULT] settings in exactly one place (jail.local or something like jail.d/00-defaults.local). The full load order is on the configuration overview.
Check that jail.local is applied#
sudo fail2ban-client -t # syntax and filter check
sudo fail2ban-client reload # apply
sudo fail2ban-client status # list of running jails
sudo fail2ban-client get sshd maxretry # value the server is using$ sudo fail2ban-client status
Status
|- Number of jail: 2
`- Jail list: recidive, sshdjail.local not working? Checklist#
- Wrong place or name. It must be exactly
/etc/fail2ban/jail.local(watch out forjail.local.txtor a copy in your home directory). - Not reloaded. Run
sudo fail2ban-client reloadafter every change. - Overridden later. A
jail.d/*.localfile loads afterjail.localand wins. Search:grep -rn "bantime" /etc/fail2ban/jail.d/. - Missing
enabled = true, or a typo in the section name ([ssh]instead of[sshd]). An unknown section is a new jail with a filter that doesn't exist. - Option in the wrong section. Everything after a
[sshd]header belongs tosshduntil the next header. Put global options above the first jail. - Backend vs logpath mismatch. With
backend = systemd,logpathis ignored; with a file backend, the log file must exist. - Inline comment with
#. Only;(with a space before it) starts an inline comment.
sudo fail2ban-client -d | grep "'sshd'" shows the values fail2ban really uses. For start-up errors see config errors.
FAQ#
Where is jail.local located?#
At /etc/fail2ban/jail.local. It doesn't exist after installation; you create it. fail2ban reads it after jail.conf and jail.d/*.conf, so its values override the defaults. Files in /etc/fail2ban/jail.d/ ending in .local are read after it.
Should I copy jail.conf to jail.local?#
No. Create an empty jail.local and add only the settings you change. A full copy overrides every stock value, so fixes that upgrades bring to jail.conf (new log paths, filter options) never reach you.
How do I add a new jail in fail2ban?#
Add a section to jail.local with enabled = true, the port to block, the logpath (or backend = systemd) and, if the name differs from the filter file, filter = name. Test the filter with fail2ban-regex, then run sudo fail2ban-client reload.
Why is my jail.local not working?#
Usually it wasn't reloaded, the jail lacks enabled = true, the section name has a typo, or a jail.d/*.local file overrides the same option. Run sudo fail2ban-client -t for syntax errors and sudo fail2ban-client -d to see the values fail2ban actually applies.
Can I use jail.local and jail.d at the same time?#
Yes. fail2ban reads jail.conf, then jail.d/*.conf, then jail.local, then jail.d/*.local. Later files win for the same option in the same section. Just don't set the same option in two places, or you'll lose track of which one applies.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.