Guides
fail2ban with Nginx Proxy Manager
Nginx Proxy Manager (NPM) writes its own log format to its own files inside a Docker volume, so stock nginx filters don't fit and a normal ban doesn't block anything. This guide fixes both with a tested filter and a DOCKER-USER jail.
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.
[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 = 1hReplace /opt/npm with the directory that holds your NPM compose.yaml (or docker-compose.yml).
Where Nginx Proxy Manager keeps its logs#
NPM's standard compose file mounts ./data to /data in the container:
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencryptNPM writes one access and one error log per proxy host, numbered by the host's ID in the UI:
| 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:
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';A request looks like this:
[25/Oct/2026:18:36:10 +0000] - 404 404 - GET https cloud.example.com "/.env" [Client 203.0.113.42] [Length 146] [Gzip -] [Sent-to 172.18.0.5] "Mozilla/5.0" "-"
[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" "-"The fields before - are the cache status, the backend's status (- when NPM answered itself, for example an Access List 401) and the status sent to the client. The filter above counts the third one and takes the address from [Client …]. That's why stock filters such as nginx-bad-request don't match NPM's access logs: they expect the IP at the start of the line.
Test the filter#
sudo fail2ban-regex /opt/npm/data/logs/proxy-host-1_access.log /etc/fail2ban/filter.d/npm-4xx.confWe ran the filter against the sample lines above plus a 200, an IPv6 403 and a /favicon.ico 404: the 404, 401 and 403 lines matched, the favicon request was ignored and the 200 missed — that's what you want to see. To ban only 401/403 (failed logins) and leave 404s alone, use filter = npm-4xx[codes="401|403"]. Tweak and retest in the regex tester if you've customized NPM's log format.
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 works:
[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 = 5Why chain = DOCKER-USER (and why INPUT doesn't work)#
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:
sudo iptables -L DOCKER-USER -n --line-numbers
sudo fail2ban-client status npmIf your host uses Docker's experimental nftables firewall backend, there's no DOCKER-USER chain; use the nftables forward-hook variant from fail2ban with Docker containers.
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.
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, subfolderlogs; community images may use a different layout, so check the container's path mappings and look forproxy-host-*_access.log. Run fail2ban as a container with host networking andNET_ADMIN, as described below. If the container has its own IP on a custombr0(macvlan) network, its traffic doesn't pass the host'sFORWARDchain, soDOCKER-USERbans 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.
Nginx Proxy Manager behind Cloudflare#
Two things change when Cloudflare proxies your domains:
- 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 theX-Real-IPheader, 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'sserverblock — and restart NPM:
real_ip_header CF-Connecting-IP;- 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-tokenaction instead — set up in the Cloudflare guide.
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.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.