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.

Updated 9 min readChecked against fail2ban 1.1.1

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:

/etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
bantime  = 1h

[sshd]
enabled = true
bash
sudo fail2ban-client -t && sudo fail2ban-client reload
sudo fail2ban-client status sshd

Replace 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
#
# /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,https

Prefer 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. ssh resolves 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's journalmatch. Required on systems without rsyslog (Debian 12 has no /var/log/auth.log by default). With this backend any logpath is 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:

ini
[nginx-http-auth]
enabled = true
port    = http,https
logpath = /var/log/nginx/*error.log
          /srv/www/*/logs/error.log

Behind 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 = systemd in [DEFAULT], and [sshd] enabled = true. Because the global backend is systemd, file-based jails like nginx read nothing until you set backend = auto, as the example does.
  • Debian 13 / Ubuntu 26.04 (1.1.0): nftables actions, plus [sshd] backend = systemd with journalmatch = _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.log is missing and sshd fails with "Have not found any log file". Fix: sudo apt install python3-systemd and backend = systemd in [sshd].
bash
cat /etc/fail2ban/jail.d/defaults-debian.conf

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:

/etc/fail2ban/jail.local
[postfix-sasl]
enabled = true

For your own application, write a filter filter.d/myapp.conf (see writing a custom filter) and point a custom jail at it:

/etc/fail2ban/jail.local
[myapp]
enabled  = true
filter   = myapp
logpath  = /var/log/myapp/auth.log
port     = http,https
maxretry = 5
backend  = auto

filter = 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:

bash
sudo fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conf

Use 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:

/etc/fail2ban/jail.d/sshd.local
[sshd]
enabled  = true
maxretry = 3
backend  = systemd
/etc/fail2ban/jail.d/nginx.local
[nginx-http-auth]
enabled = true

[nginx-botsearch]
enabled = true

Keep [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#

bash
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
output
$ sudo fail2ban-client status
Status
|- Number of jail:	2
`- Jail list:	recidive, sshd

jail.local not working? Checklist#

  1. Wrong place or name. It must be exactly /etc/fail2ban/jail.local (watch out for jail.local.txt or a copy in your home directory).
  2. Not reloaded. Run sudo fail2ban-client reload after every change.
  3. Overridden later. A jail.d/*.local file loads after jail.local and wins. Search: grep -rn "bantime" /etc/fail2ban/jail.d/.
  4. 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.
  5. Option in the wrong section. Everything after a [sshd] header belongs to sshd until the next header. Put global options above the first jail.
  6. Backend vs logpath mismatch. With backend = systemd, logpath is ignored; with a file backend, the log file must exist.
  7. 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.