# fail2ban jail.local example (annotated)

> A complete fail2ban jail.local example for SSH, nginx and recidive, explained line by line, plus a minimal version, distro notes and how to add a custom jail.
>
> Source: https://fail2ban.dev/config/jail-local/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

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

```ini
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
bantime  = 1h

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

## Complete jail.local example

This is a production-ready starting point for a typical VPS running SSH and nginx. Each block is explained below.

```ini
#
# /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
```

## 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](/config/bantime-findtime-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](/config/bantime-increment/). |
| `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](/config/banaction/). |
| `action = %(action_)s` | Ban only. `%(action_mw)s` also emails a whois report and `%(action_mwl)s` adds the log lines ([email alerts](/config/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](/guides/ssh/#filter-sshd) and in the [SSH guide](/guides/ssh/).

### [recidive]: long bans for repeat offenders

The stock [recidive jail](/config/recidive/) 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
enabled = true
port    = http,https
logpath = /var/log/nginx/*error.log
          /srv/www/*/logs/error.log
```

## Distro-specific notes


#### Ubuntu / Debian
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
```
fail2ban 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](/install/rhel/).

```bash
```
Same 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]`.
#### Arch Linux
`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](/install/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:

```ini
[postfix-sasl]
enabled = true
```

```ini
[myapp]
enabled  = true
filter   = myapp
logpath  = /var/log/myapp/auth.log
port     = http,https
maxretry = 5
backend  = auto
```

```bash
```

The same configuration can be split into one file per jail. fail2ban reads `jail.d/*.local` after `jail.local`, alphabetically:

```ini
[sshd]
enabled  = true
maxretry = 3
backend  = systemd
```
[nginx-http-auth]
enabled = true

[nginx-botsearch]
enabled = true
```

## Check that jail.local is applied

```bash
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, sshd
```

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](/troubleshooting/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.
