# fail2ban with Nginx Proxy Manager

> Add fail2ban to Nginx Proxy Manager in Docker: NPM log paths and format, a tested 401/403/404 filter, the DOCKER-USER jail, Cloudflare, Unraid and Proxmox.
>
> Source: https://fail2ban.dev/guides/nginx-proxy-manager/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

To add fail2ban to Nginx Proxy Manager, run fail2ban on the Docker host, point a custom filter at NPM's logs in `./data/logs/proxy-host-*_access.log` (NPM's `/data/logs` volume), match the `[Client <IP>]` field with status 401, 403 or 404, and ban with `banaction = iptables-allports` plus `chain = DOCKER-USER`. A ban in the default `INPUT` chain is ignored by traffic Docker forwards to the NPM container.

```ini
[Definition]
failregex = ^\[\] \S+ \S+ (?:<codes>) - [A-Z]+ \S+ \S+ "[^"]*" \[Client <ADDR>\]
ignoreregex = ^\[\] \S+ \S+ \d+ - (?:GET|HEAD) \S+ \S+ "/(?:favicon\.ico|robots\.txt)"
datepattern = ^\[({DATE})

[Init]
codes = 401|403|404
```
[npm]
enabled   = true
backend   = auto
filter    = npm-4xx
logpath   = /opt/npm/data/logs/proxy-host-*_access.log
banaction = iptables-allports
chain     = DOCKER-USER
maxretry  = 10
findtime  = 10m
bantime   = 1h
```

## Where Nginx Proxy Manager keeps its logs

NPM's standard compose file mounts `./data` to `/data` in the container:

```yaml
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
```

| File (inside the container) | Contents |
|---|---|
| `/data/logs/proxy-host-1_access.log` | Requests to proxy host 1 |
| `/data/logs/proxy-host-1_error.log` | nginx errors for proxy host 1, incl. Access List auth failures |
| `/data/logs/fallback_http_access.log` | Requests that matched no host |
| `/data/logs/fallback_error.log` | Global nginx errors |

On the host they're in `./data/logs/` next to your compose file. List them with `ls /opt/npm/data/logs/`. fail2ban expands `proxy-host-*_access.log` when the jail starts, so **reload fail2ban after adding a proxy host** in the NPM UI: `sudo fail2ban-client reload npm`.

## NPM's log format

NPM doesn't use nginx's `combined` format. Its `proxy` format, from NPM's `log-proxy.conf`, is:

```nginx
```

```log
[25/Oct/2026:18:36:11 +0000] - - 401 - GET https app.example.com "/" [Client 203.0.113.42] [Length 179] [Gzip -] [Sent-to 172.18.0.6] "Mozilla/5.0" "-"
```

## Test the filter

```bash
```

## Access List (basic auth) brute force

Proxy hosts protected with an NPM Access List use nginx `auth_basic`. Failed logins land in the host's **error** log in the standard nginx format, so the stock [`nginx-http-auth` filter](/guides/nginx/#filter-nginx-http-auth) works:

```ini
[npm-auth]
enabled   = true
backend   = auto
filter    = nginx-http-auth
logpath   = /opt/npm/data/logs/proxy-host-*_error.log
banaction = iptables-allports
chain     = DOCKER-USER
maxretry  = 5
```

Docker publishes NPM's ports 80, 81 and 443 with DNAT; packets are forwarded to the container and pass the `FORWARD` chain, never `INPUT`. fail2ban's default ban sits in `INPUT` (or the nftables `input` hook), so it shows up in `fail2ban-client status` but blocks nothing. `DOCKER-USER` is the chain Docker evaluates first for forwarded traffic. `iptables-allports` blocks every port, so you don't need to worry that the rule sees container ports after DNAT.

Make sure fail2ban starts after Docker (`sudo systemctl edit fail2ban`, add `After=docker.service` under `[Unit]`), and confirm the rule after the first ban:

```bash
sudo fail2ban-client status npm
```

## Run fail2ban on the host or in a container?

**On the host** is simplest: `apt install fail2ban`, the logs are already on disk, and fail2ban manipulates the host firewall directly. **In a container** (`crazymax/fail2ban`, `linuxserver/fail2ban`) works too, but the container needs `network_mode: host`, the `NET_ADMIN` and `NET_RAW` capabilities, and NPM's `data/logs` mounted read-only, for example `- /opt/npm/data/logs:/remotelogs/npm:ro`; the `logpath` in the jail then uses the path inside the fail2ban container. Running fail2ban inside the NPM container itself isn't practical: it has no access to the host's firewall. See [install fail2ban with Docker](/guides/docker/).

## Unraid and Proxmox

- **Unraid**: NPM runs as a Docker app with its data under `/mnt/user/appdata/`. With the official image the logs are in the folder mapped to `/data`, subfolder `logs`; community images may use a different layout, so check the container's path mappings and look for `proxy-host-*_access.log`. Run fail2ban as a container with host networking and `NET_ADMIN`, as described below. If the container has its own IP on a custom `br0` (macvlan) network, its traffic doesn't pass the host's `FORWARD` chain, so `DOCKER-USER` bans won't apply; use bridge networking with published ports, or ban at the router or at Cloudflare.
- **Proxmox**: if NPM runs in an LXC container or VM, install fail2ban **inside** that guest, where NPM's logs and network stack are, and use the default `INPUT`-based ban unless NPM runs in Docker inside the guest. Protecting the Proxmox web UI itself is a separate jail: [Proxmox guide](/guides/proxmox/).

## Nginx Proxy Manager behind Cloudflare

Two things change when Cloudflare proxies your domains:

1. **Real IP.** NPM trusts Cloudflare's published ranges (it downloads them at startup unless `IP_RANGES_FETCH_ENABLED=false`) but reads the client address from the `X-Real-IP` header, which Cloudflare doesn't send. Without a fix, `[Client …]` shows Cloudflare addresses and fail2ban would ban Cloudflare. Create `./data/nginx/custom/server_proxy.conf` — NPM includes it in every proxy host's `server` block — and restart NPM:

   ```nginx title="data/nginx/custom/server_proxy.conf"
   real_ip_header CF-Connecting-IP;
   ```

2. **Where to ban.** Visitors connect to Cloudflare, and Cloudflare connects to you. A firewall ban on your host blocks Cloudflare's edge server, not the attacker, who simply keeps going through Cloudflare. Ban at Cloudflare's edge with the `cloudflare-token` action instead — set up in the [Cloudflare guide](/guides/cloudflare/).

## FAQ

### Can fail2ban work with Nginx Proxy Manager?

Yes. Run fail2ban on the Docker host, read NPM's logs from the `data/logs` volume with a filter that understands NPM's `[Client <IP>]` format, and ban with `chain = DOCKER-USER` and an iptables action so bans apply to traffic forwarded to the NPM container.

### Where are the Nginx Proxy Manager logs?

Inside the container in `/data/logs/`: `proxy-host-<id>_access.log` and `proxy-host-<id>_error.log` per proxy host, plus `fallback_http_access.log` and `fallback_error.log`. With the standard compose file they're in `./data/logs/` on the host, next to `compose.yaml`.

### Why does fail2ban ban IPs but NPM still serves them?

Because the ban is in the `INPUT` chain, and Docker forwards traffic to the NPM container through `FORWARD`. Set `banaction = iptables-allports` and `chain = DOCKER-USER` in the jail, reload fail2ban, and check `iptables -L DOCKER-USER -n` for the `f2b-` jump.

### Which filter should I use for NPM?

NPM's access logs use a custom format, so write a filter that matches `[Client <ADDR>]` and the status code, like `npm-4xx` on this page. For Access List (basic auth) failures, the stock `nginx-http-auth` filter works on the `proxy-host-*_error.log` files.

### Do I need to reload fail2ban when I add a proxy host in NPM?

Yes. fail2ban expands the `proxy-host-*_access.log` wildcard only when the jail starts, so a new host's log file isn't watched until you run `sudo fail2ban-client reload npm`. Existing files keep being followed across log rotation.
