Guides
fail2ban for nginx
fail2ban ships five nginx filters. This guide shows which log each one reads, the nginx config some of them need, a custom filter for 404/401/403 floods, and how to keep the real client IP when nginx sits behind a proxy.
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.
[nginx-http-auth]
enabled = true
backend = auto
[nginx-botsearch]
enabled = true
backend = auto
[nginx-bad-request]
enabled = true
backend = autosudo fail2ban-client reload
sudo fail2ban-client statusEach jail inherits port = http,https and the right logpath from jail.conf, so enabled and backend are all you need for a standard Debian, Ubuntu or RHEL nginx install.
The stock nginx jails at a glance#
| Jail | Reads | Bans clients that… | Needs nginx config? |
|---|---|---|---|
nginx-http-auth |
error log | fail HTTP basic auth (wrong password, unknown user) | Only auth_basic |
nginx-limit-req |
error log | exceed a limit_req (or limit_conn) zone |
Yes, a rate limit |
nginx-botsearch |
error log | request well-known exploit URLs that don't exist | No |
nginx-bad-request |
access log | send malformed requests answered with 400 | No |
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:
2026/10/25 18:36:10 [error] 1234#1234: *5678 user "admin": password mismatch, client: 203.0.113.42, server: example.com, request: "GET /admin/ HTTP/1.1", host: "example.com"
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"The jail catches both lines. In 1.1.1 the filter also matches PAM authentication failures, and mode = aggressive (or fallback) adds repeated TLS handshake failures (SSL_do_handshake() failed):
[nginx-http-auth]
enabled = true
backend = auto
maxretry = 3
bantime = 1hnginx-limit-req: rate limiting plus bans#
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:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=1r/s;location /login {
limit_req zone=req_limit burst=5 nodelay;
# ...
}Each rejected request is logged like this:
2026/10/25 18:36:10 [error] 1234#1234: *5678 limiting requests, excess: 10.520 by zone "req_limit", client: 203.0.113.42, server: example.com, request: "POST /login HTTP/1.1", host: "example.com"[nginx-limit-req]
enabled = true
backend = auto
findtime = 1m
maxretry = 10
bantime = 1hNotes:
- The filter also matches
limiting connectionsfromlimit_conn(1.1.1) anddelaying requestlines. nginx logs delays one level below refusals, so with the defaulterror_log … errorlevel 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/slimit 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:
2026/10/25 18:36:10 [error] 1234#1234: *5678 open() "/var/www/html/wp-login.php" failed (2: No such file or directory), client: 203.0.113.42, server: example.com, request: "GET /wp-login.php HTTP/1.1", host: "example.com"nginx only writes these when log_not_found is on (the default). Don't enable this jail on a server that actually runs WordPress or phpMyAdmin — it's meant for sites where those URLs are always bogus. It won't catch generic /.env or /.git/config probes; that's what the 404 filter below is for.
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:
203.0.113.42 - - [25/Oct/2026:18:36:10 +0000] "\x16\x03\x01\x02\x00\x01\x00\x01\xFC\x03\x03" 400 157 "-" "-"nginx-forbidden counts requests for locations you've blocked with deny, for example dotfiles:
location ~ /\. {
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"Ban 404, 401 and 403 floods with a custom filter#
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:
[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 = 10It matches lines such as:
203.0.113.42 - - [25/Oct/2026:18:36:10 +0000] "GET /.env HTTP/1.1" 404 153 "-" "Mozilla/5.0"Keep maxretry for 404s generous: a broken image link on your own page produces 404s for every real visitor. Test against your own log first (see below) and add paths that legitimately 404 to ignoreregex. Writing a custom filter helps when your log_format differs from combined.
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):
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;Only list addresses you control; anyone in a trusted range can forge the header. Behind Cloudflare, use CF-Connecting-IP and Cloudflare's ranges instead — and note that a local firewall ban doesn't stop traffic that comes through Cloudflare's network. Both problems are covered in the Cloudflare guide and fail2ban bans the proxy IP.
Banning inside nginx: the nginx-block-map action#
Normally fail2ban blocks at the firewall. The nginx-block-map action 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:
sudo fail2ban-regex /var/log/nginx/error.log nginx-http-auth
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.confLook for Lines: … matched in the summary. Then confirm the running jails see the files:
sudo fail2ban-client status nginx-http-auth
sudo fail2ban-client get nginx-404 logpathTo trigger a test ban, request a protected page with wrong credentials from another IP: curl -u admin:wrong https://example.com/admin/ a few times, then check sudo tail /var/log/fail2ban.log for Ban. If nginx runs in Docker, a ban on the host's INPUT chain won't stop it — see fail2ban with Docker containers.
Built-in filter reference: nginx-http-auth, nginx-botsearch, nginx-limit-req and more#
The built-in filters this guide uses, exactly as fail2ban 1.1.1 ships them — with the log lines they match and a link to test them. Looking for something else? See HAProxy, lighttpd, Squid and the other web server, proxy and web-app filters, or the full filter library.
nginx-http-auth nginx
The nginx-http-auth filter bans failed HTTP Basic authentication (auth_basic) in the nginx error log. File: /etc/fail2ban/filter.d/nginx-http-auth.conf; jail [nginx-http-auth]; log on Debian/Ubuntu: /var/log/nginx/*error.log; modes: aggressive, auth, fallback, normal.
Log lines it bans (from fail2ban's test suite):
2012/04/09 11:53:29 [error] 2865#0: *66647 user "xyz" was not found in "/var/www/.htpasswd", client: 192.0.43.10, server: www.myhost.com, request: "GET / HTTP/1.1", host: "www.myhost.com"2012/04/09 11:53:36 [error] 2865#0: *66647 user "xyz": password mismatch, client: 192.0.43.10, server: www.myhost.com, request: "GET / HTTP/1.1", host: "www.myhost.com"2014/04/01 22:20:38 [error] 30708#0: *3 user "scribendio": password mismatch, client: 10.0.2.2, server: , request: "GET / HTTP/1.1", host: "localhost:8443"
Show the failregex fail2ban compiles (2 lines)
^\s*\[(?:error|crit)\] \d+#\d+: \*\d+\s+<F-CONTENT>.*</F-CONTENT>, client: <ADDR>(?:, (?:server|request|host|referrer): (?:"[^"]*"|\S*)){0,4}\s*$^user "<F-USER>(?:[^"]+|.*?)</F-USER>":? (?:password mismatch|was not found in "[^\"]*")$
^(?:PAM: )?user '<F-USER>(?:[^']+|.*?)</F-USER>' - not authenticated: Authentication failure$Test nginx-http-auth in the browser or on the server: sudo fail2ban-regex /var/log/nginx/*error.log nginx-http-auth · source
nginx-botsearch nginx
The nginx-botsearch filter bans scanners requesting admin pages and scripts that don't exist (404s for wp-login, phpMyAdmin and similar). File: /etc/fail2ban/filter.d/nginx-botsearch.conf; jail [nginx-botsearch]; log on Debian/Ubuntu: /var/log/nginx/*error.log.
Log lines it bans (from fail2ban's test suite):
12.34.56.78 - - [20/Jan/2015:19:53:28 +0100] "GET //phpMyAdmin-2.8.2.3/scripts/setup.php HTTP/1.1" 404 47 "-" "-" "-"12.34.56.78 - - [20/Jan/2015:19:53:28 +0100] "GET //pma/scripts/setup.php HTTP/1.1" 404 47 "-" "-" "-"12.34.56.78 - - [20/Jan/2015:19:53:28 +0100] "GET //mysqladmin/scripts/setup.php HTTP/1.1" 404 47 "-" "-" "-"
Show the failregex fail2ban compiles (2 lines)
^<HOST> \- \S+ \[\] \"(GET|POST|HEAD) \/\/?(roundcube|(ext)?mail|horde|(v-?)?webmail|(typo3/|xampp/|admin/|)(pma|(php)?[Mm]y[Aa]dmin)|wp-(login|signup|admin)\.php|cgi-bin|mysqladmin)[^,]* \S+\" 404 .+$
^ \[error\] \d+#\d+: \*\d+ (\S+ )?\"\S+\" (failed|is not found) \(2\: No such file or directory\), client\: <HOST>\, server\: \S*\, request: \"(GET|POST|HEAD) \/\/?(roundcube|(ext)?mail|horde|(v-?)?webmail|(typo3/|xampp/|admin/|)(pma|(php)?[Mm]y[Aa]dmin)|wp-(login|signup|admin)\.php|cgi-bin|mysqladmin)[^,]* \S+\"\, .*?$Test nginx-botsearch in the browser or on the server: sudo fail2ban-regex /var/log/nginx/*error.log nginx-botsearch · source
nginx-limit-req nginx
The nginx-limit-req filter bans clients that keep tripping nginx limit_req rate limits. File: /etc/fail2ban/filter.d/nginx-limit-req.conf; jail [nginx-limit-req]; log on Debian/Ubuntu: /var/log/nginx/*error.log.
Log lines it bans (from fail2ban's test suite):
2015/10/29 20:01:02 [error] 256554#0: *99927 limiting requests, excess: 1.852 by zone "one", client: 1.2.3.4, server: example.com, request: "POST /index.htm HTTP/1.0", host: "exmaple.com"2015/10/29 19:24:05 [error] 12684#12684: *22174 limiting requests, excess: 1.495 by zone "one", client: 192.0.2.0, server: example.com, request: "GET /index.php HTTP/1.1", host: "example.com", referrer: "https://example.com"2016/09/30 08:36:06 [error] 22923#0: *4758725916 limiting requests, excess: 15.243 by zone "one", client: 13.123.1.123, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Show the failregex fail2ban compiles (1 line)
^\s*\[[a-z]+\] \d+#\d+: \*\d+\s+(?:limiting|delaying) (?:request|connection)s?(?:, excess: [\d\.]+,?)? by zone "(?:[^"]+)", client: <ADDR>,Test nginx-limit-req in the browser or on the server: sudo fail2ban-regex /var/log/nginx/*error.log nginx-limit-req · source
nginx-bad-request nginx
The nginx-bad-request filter bans clients sending malformed requests that nginx answers with 400 Bad Request. File: /etc/fail2ban/filter.d/nginx-bad-request.conf; jail [nginx-bad-request]; log on Debian/Ubuntu: /var/log/nginx/*access.log.
Log lines it bans (from fail2ban's test suite):
12.34.56.78 - - [20/Jan/2015:19:53:28 +0100] "" 400 47 "-" "-" "-"12.34.56.78 - root [20/Jan/2015:19:53:28 +0100] "" 400 47 "-" "-" "-"12.34.56.78 - - [20/Jan/2015:19:53:28 +0100] "\x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr" 400 47 "-" "-" "-"
Show the failregex fail2ban compiles (1 line)
^<HOST> - \S+ \[\] "[^"]*" 400Test nginx-bad-request in the browser or on the server: sudo fail2ban-regex /var/log/nginx/*access.log nginx-bad-request · source
nginx-forbidden nginx
The nginx-forbidden filter bans clients that keep hitting locations you deny in nginx (403 forbidden by rule). File: /etc/fail2ban/filter.d/nginx-forbidden.conf; jail [nginx-forbidden]; log on Debian/Ubuntu: /var/log/nginx/*error.log.
Log lines it bans (from fail2ban's test suite):
2018/09/14 19:03:05 [error] 2035#2035: *9134 access forbidden by rule, client: 12.34.56.78, server: www.example.net, request: "GET /wp-content/themes/evolve/js/back-end/libraries/fileuploader/upload_handler.php HTTP/1.1", host: "www.example.net", referrer: "http://example.net/foo.php"2018/09/13 15:42:05 [error] 2035#2035: *287 access forbidden by rule, client: 12.34.56.78, server: www.example.com, request: "GET /wp-config.php~ HTTP/1.1", host: "www.example.com"
Show the failregex fail2ban compiles (1 line)
^\s*\[error\] \d+#\d+: \*\d+\s+access forbidden by rule, client: <HOST>Test nginx-forbidden in the browser or on the server: sudo fail2ban-regex /var/log/nginx/*error.log nginx-forbidden · source
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.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.