# fail2ban for nginx

> Set up fail2ban for nginx: nginx-http-auth, nginx-botsearch, nginx-limit-req with limit_req_zone, bad requests, forbidden, a 404 filter and real IPs behind a proxy.
>
> Source: https://fail2ban.dev/guides/nginx/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

To use fail2ban with nginx, enable the stock nginx jails in `/etc/fail2ban/jail.local` — `nginx-http-auth`, `nginx-botsearch`, `nginx-limit-req`, `nginx-bad-request` and `nginx-forbidden` — with `backend = auto` so fail2ban reads `/var/log/nginx/*error.log` and `*access.log`. Reload with `sudo fail2ban-client reload` and check with `sudo fail2ban-client status nginx-http-auth`. For 404 scanners, add the small custom filter below.

```ini
[nginx-http-auth]
enabled = true
backend = auto

[nginx-botsearch]
enabled = true
backend = auto

[nginx-bad-request]
enabled = true
backend = auto
```
sudo fail2ban-client reload
sudo fail2ban-client status
```

> **Why backend = auto?**
Ubuntu 24.04 sets `backend = systemd` for all jails in `/etc/fail2ban/jail.d/defaults-debian.conf`. With the systemd backend, fail2ban ignores `logpath` and looks for nginx messages in the journal, where nginx doesn't write its access or error log. Setting `backend = auto` per jail makes fail2ban read the files. It's harmless on other distributions.

## The stock nginx jails at a glance

| Jail | Reads | Bans clients that… | Needs nginx config? |
|---|---|---|---|
| [`nginx-http-auth`](/guides/nginx/#filter-nginx-http-auth) | error log | fail HTTP basic auth (wrong password, unknown user) | Only `auth_basic` |
| [`nginx-limit-req`](/guides/nginx/#filter-nginx-limit-req) | error log | exceed a `limit_req` (or `limit_conn`) zone | Yes, a rate limit |
| [`nginx-botsearch`](/guides/nginx/#filter-nginx-botsearch) | error log | request well-known exploit URLs that don't exist | No |
| [`nginx-bad-request`](/guides/nginx/#filter-nginx-bad-request) | access log | send malformed requests answered with 400 | No |
| [`nginx-forbidden`](/guides/nginx/#filter-nginx-forbidden) | error log | hit locations you `deny` | Yes, `deny` rules |

Default log paths (from `paths-common.conf`): `nginx_error_log = /var/log/nginx/*error.log`, `nginx_access_log = /var/log/nginx/*access.log`. The glob catches per-site logs like `example.com.error.log` too, but only files that exist when the jail starts — reload fail2ban after adding a new site with its own log file.

## nginx-http-auth: brute force on basic auth

When a client fails `auth_basic`, nginx writes to the error log:

```log
2026/10/25 18:36:12 [error] 1234#1234: *5679 user "root" was not found in "/etc/nginx/.htpasswd", client: 203.0.113.42, server: example.com, request: "GET /admin/ HTTP/1.1", host: "example.com"
```

```ini
[nginx-http-auth]
enabled  = true
backend  = auto
maxretry = 3
bantime  = 1h
```

nginx's `limit_req` module rejects clients that send requests too fast; fail2ban turns repeat offenders into firewall bans, so nginx no longer has to answer them at all. First define a zone and apply it — the example from the filter's own documentation:

```nginx
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=1r/s;
```
location /login {
    limit_req zone=req_limit burst=5 nodelay;
    # ...
}
```

```log
```
[nginx-limit-req]
enabled  = true
backend  = auto
findtime = 1m
maxretry = 10
bantime  = 1h
```

- The filter also matches `limiting connections` from `limit_conn` (1.1.1) and `delaying request` lines. nginx logs delays one level below refusals, so with the default `error_log … error` level only refusals reach the log.
- To ban only for specific zones, pass them to the filter: `filter = nginx-limit-req[ngx_limit_req_zones="req_limit|login_zone"]`.
- Rate-limit dynamic endpoints (login, search, API) rather than your whole site; a page with 60 images would trip a site-wide `1r/s` limit for every normal visitor.

## nginx-botsearch: exploit scanners

`nginx-botsearch` bans clients asking for a fixed list of commonly probed paths that don't exist on your server: webmail (roundcube, horde…), phpMyAdmin variants, `wp-login.php`/`wp-admin.php`/`wp-signup.php`, `cgi-bin` and `mysqladmin`. It reads nginx's "file not found" error lines:

```log
```

## nginx-bad-request and nginx-forbidden

`nginx-bad-request` reads the **access log** and counts requests nginx answered with `400` — typically TLS handshakes sent to plain HTTP, RDP or SMB probes:

```log
```

```nginx
    deny all;
}
```
2026/10/25 18:36:10 [error] 1234#1234: *5678 access forbidden by rule, client: 203.0.113.42, server: example.com, request: "GET /.env HTTP/1.1", host: "example.com"
```

No stock filter bans plain 404 scanning, so add your own. This one reads the default `combined` access log and lets each jail choose the status codes:

```ini
[Definition]
failregex = ^<HOST> \S+ \S+ \[\] "[A-Z]+ [^"]*" (?:<codes>)\s
ignoreregex = ^<HOST> \S+ \S+ \[\] "(?:GET|HEAD) /(?:favicon\.ico|robots\.txt|apple-touch-icon\S*\.png)\s
datepattern = ^[^\[]*\[({DATE})
              {^LN-BEG}

[Init]
codes = 401|403|404
```
[nginx-404]
enabled  = true
backend  = auto
filter   = nginx-4xx[codes="404"]
logpath  = /var/log/nginx/access.log
port     = http,https
findtime = 10m
maxretry = 20
bantime  = 1h

[nginx-unauthorized]
enabled  = true
backend  = auto
filter   = nginx-4xx[codes="401|403"]
logpath  = /var/log/nginx/access.log
port     = http,https
maxretry = 10
```

```log
```

## Can fail2ban stop an nginx DDoS?

Partly. fail2ban reacts to log lines, so it works against a handful of addresses sending lots of requests — exactly what `nginx-limit-req` catches. A distributed attack from thousands of IPs, or a bandwidth flood, is out of its reach: by the time a log line is written the traffic has already arrived. Use nginx rate limiting plus fail2ban for small floods, and an upstream service (your host's DDoS protection, or Cloudflare with edge bans) for real volumetric attacks.

## nginx behind a reverse proxy: log the real client IP

If nginx sits behind another proxy, a load balancer or a CDN, every log line contains the proxy's address and fail2ban bans the proxy — taking the whole site down for everyone. Make nginx replace `$remote_addr` with the client address from the trusted proxy's header (module `ngx_http_realip_module`, included in Debian/Ubuntu and nginx.org packages):

```nginx
set_real_ip_from 10.0.0.5;          # your proxy / load balancer
set_real_ip_from 192.168.1.0/24;
real_ip_header   X-Forwarded-For;
real_ip_recursive on;
```

## Banning inside nginx: the nginx-block-map action

Normally fail2ban blocks at the firewall. The [`nginx-block-map` action](/actions/#action-nginx-block-map) instead writes banned values into a file nginx includes as a `map`, so nginx itself returns 403. It's designed for banning things that aren't IP addresses, such as session cookies or API tokens, and needs matching `map` and `location` blocks in nginx; the action file documents a full example.

## Test your nginx jails

Run each filter against the log it will read before you trust it:

```bash
sudo fail2ban-regex /var/log/nginx/error.log nginx-limit-req
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf
```

```bash
sudo fail2ban-client get nginx-404 logpath
```

## Built-in filter reference: nginx-http-auth, nginx-botsearch, nginx-limit-req and more

{{component:filter-ref:nginx-http-auth.nginx-botsearch.nginx-limit-req.nginx-bad-request.nginx-forbidden}}

## FAQ

### Which fail2ban filters exist for nginx?

fail2ban 1.1.1 ships `nginx-http-auth` (basic auth failures), `nginx-limit-req` (rate-limit and connection-limit rejections), `nginx-botsearch` (probes for common exploit URLs), `nginx-bad-request` (400 responses) and `nginx-forbidden` (`deny` rule hits). Each has a disabled jail of the same name in `jail.conf` that you enable in `jail.local`.

### How do I ban 404 errors in nginx with fail2ban?

Create a filter that matches status 404 in the access log, for example `failregex = ^<HOST> \S+ \S+ \[\] "[A-Z]+ [^"]*" 404\s`, and a jail pointing at `/var/log/nginx/access.log` with a high `maxretry` such as 20. Ignore `favicon.ico` and `robots.txt` so normal browsers aren't counted.

### Does nginx-limit-req work without limit_req in nginx?

No. The filter only reads the "limiting requests" lines nginx writes when a `limit_req` zone rejects a request. Define `limit_req_zone` in the `http` block and `limit_req` in a server or location, reload nginx, and only then enable the `nginx-limit-req` jail.

### Why does fail2ban ban my reverse proxy instead of the attacker?

Because nginx logs the address that connected to it, which is the proxy. Configure `set_real_ip_from` with the proxy's IP and `real_ip_header X-Forwarded-For` (or `CF-Connecting-IP` for Cloudflare) so `$remote_addr` becomes the client address, then fail2ban sees and bans the real attacker.

### Why is my nginx jail not finding any failures?

Check that the jail reads the right file (`fail2ban-client get <jail> logpath`), that `backend = auto` is set on Ubuntu 24.04, and that the filter matches your log format with `fail2ban-regex`. A custom `log_format` that doesn't start with the client IP breaks the access-log filters.
