Fail2ban for Nginx: Auto-Ban CC Attacks and Scans
Manual log review then firewall rules is too slow. Fail2ban tails Nginx logs and bans automatically. Covers filter regex, jail thresholds, CC high-rate and 4xx scan dual rules, iptables ban verification, plus log-format mismatch and CDN false-ban troubleshooting. Commands are copy-paste ready.
Scope
Fail2ban started as SSH brute-force defense; the jail model applies to the web layer too. Tail Nginx logs, auto-ban IPs that match CC patterns, sensitive-path scans, or login brute force. Versus hand-writing iptables after log review, Fail2ban closes the detect–ban–unban loop. This article configures Fail2ban in front of Nginx to ban CC and malicious scans.
Prerequisites
- Linux server (Debian/Ubuntu and CentOS examples); root or sudo
- Nginx installed; access.log recording status code and URI
- Package repos reachable to install Fail2ban (yum/epeel or apt)
- Firewall tool confirmed: Fail2ban default action is iptables; CentOS 7+ on firewalld needs banaction adjusted
How it works
Three parts. filter: regexes that match abuse in a log file and extract the client IP. jail: binds a filter to a log path and a threshold—`maxretry` hits inside `findtime` triggers a ban. action: how the ban is applied; default is iptables INPUT DROP for `bantime`.
Nginx heuristics: many 4xx/5xx in a short window (scan), high-rate hits on dynamic endpoints (CC), repeated failed POSTs to login (brute force). Fail2ban tails line by line, counts matches, calls the firewall at threshold, expires the ban automatically. No human in the loop.
Steps
1. Install Fail2ban
# Debian / Ubuntu
apt update && apt install -y fail2ban
# CentOS / RHEL (requires EPEL repo)
yum install -y epel-release && yum install -y fail2ban
systemctl enable --now fail2ban
2. Write Nginx filters
Create /etc/fail2ban/filter.d/nginx-cc.conf, match high-rate hits on any page:
[Definition]
failregex = ^<HOST> -.*\"(GET|POST).*
ignoreregex =
Create /etc/fail2ban/filter.d/nginx-scan.conf, match scan/probe (4xx):
[Definition]
failregex = ^<HOST> -.*\"(GET|POST|HEAD).*(404|403|400) .*$
ignoreregex =
`<HOST>` is a Fail2ban placeholder; replaced with the log IP and captured.
3. Create jail.local
Do not edit jail.conf. Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 60
maxretry = 10
banaction = iptables-multiport
[nginx-cc]
enabled = true
port = http,https
filter = nginx-cc
logpath = /var/log/nginx/access.log
maxretry = 300
findtime = 60
bantime = 600
[nginx-scan]
enabled = true
port = http,https
filter = nginx-scan
logpath = /var/log/nginx/access.log
maxretry = 15
findtime = 60
bantime = 3600
Parameters: nginx-cc `maxretry` 300 / `findtime` 60 = one IP over 300 req/min gets 10 minutes. Tune against real traffic; false ban costs more than a miss. nginx-scan `maxretry` 15 is tighter—a real user almost never produces 15 404s in a minute; path scanners will. `bantime` can be negative (e.g. `-1`) for permanent ban; pair with an allowlist.
4. Admin allowlist and start
In `[DEFAULT]` append `ignoreip = 127.0.0.1/8 YOUR_OFFICE_IP/32`, then reload:
systemctl restart fail2ban
fail2ban-client status
Verify
From a test host, fire `ab` to trip the CC jail, then check status:
ab -n 1000 -c 10 http://TARGET_SITE/test
fail2ban-client status nginx-cc
# Expected: "Currently banned IP list" contains the test host IP
# Ban log, match detail
tail -20 /var/log/fail2ban.log
After ban the test host cannot reach the site; `bantime` expiry unbans. Confirm on the firewall:
iptables -L f2b-nginx-cc -n
FAQ
FAQ 1: Fail2ban idle while logs show abuse
Check in order: (1) wrong `logpath`—if Nginx splits logs per vhost, list every file comma-separated in the jail or use a glob; (2) log format mismatch—custom `log_format` reorders fields, `failregex` misses. Test with `fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-cc.conf`; `matched` must be > 0; (3) clock skew—system TZ must match log timestamps; `findtime` uses log time.
FAQ 2: CDN in front, origin bans CDN node IPs
Same class of bug as `limit_conn`: log IP is the CDN origin-pull node. Trip the threshold and you ban the node CIDR; every user behind it dies. Two steps: (1) Nginx realip, restore the real client IP (logs too); (2) put official CDN CIDRs in `ignoreip` as a backstop. If you cannot restore the real IP, do not ban at the network layer—use the CDN's own rate controls.
FAQ 3: Bans vanish after restart
Default bans live in memory. Restart clears them; unexpired bans lift early. In jail.local `[DEFAULT]` set `dbpurgeage` and keep sqlite persistence (default `/var/lib/fail2ban/fail2ban.sqlite3`). On restart Fail2ban restores unexpired bans from the DB. If they keep disappearing, check that partition is writable.
Summary
Fail2ban is cheap, reliable auto-ban for Nginx: filter = signature, jail = threshold and duration, action = firewall. Thresholds are the whole game—CC loose (hundreds/min), scan tight (tens and ban), admin allowlist always on. After go-live, `fail2ban-client status` daily, keep tuning. Second automatic line behind the WAF.