How to Protect nginx with fail2ban
Build custom fail2ban filters for nginx to block bad bots, brute-force attempts, and scanners — with tuned ban times and firewall backend configuration.
Before you start
- ▸nginx installed and generating access/error logs at /var/log/nginx/
- ▸An active firewall: ufw, firewalld, or nftables
- ▸sudo or root access to the server
- ▸fail2ban 0.11 or newer for bantime.increment support
fail2ban watches log files and bans IPs that show malicious patterns — repeated 404s, credential stuffing, scanner probes. nginx doesn't ship with fail2ban rules that cover modern abuse patterns, so you need custom filters. This guide builds those filters from scratch, wires them to nginx's access and error logs, and tunes ban times so legitimate traffic is never caught in the crossfire.
Prerequisites
- nginx installed and writing to
/var/log/nginx/access.logand/var/log/nginx/error.log - fail2ban installed (see install step below if not)
- A firewall already active: ufw, firewalld, or nftables
- Root or sudo access
Install fail2ban
Debian / Ubuntu
sudo apt update && sudo apt install fail2ban -y
Fedora / RHEL 9+ / Rocky Linux
sudo dnf install epel-release -y
sudo dnf install fail2ban -y
Arch Linux
sudo pacman -S fail2ban
Enable and start the service:
sudo systemctl enable --now fail2ban
Understand the Config Hierarchy
fail2ban splits configuration into .conf files (upstream defaults, never edit) and .local files (your overrides, survive upgrades). Always create or edit .local files. Filters live in /etc/fail2ban/filter.d/; jail definitions live in /etc/fail2ban/jail.local.
Create a Custom Filter: Bad Bots and Scanners
This filter catches requests from known scanners, exploit frameworks, and empty User-Agent strings. The regex matches nginx's default combined log format.
sudo nano /etc/fail2ban/filter.d/nginx-badbots.local
[Definition]
failregex = ^ .* ".*" .* "(masscan|zgrab|nikto|sqlmap|nmap|python-requests\/[0-9]|curl\/[0-9]|Go-http-client|scanbot|dirbuster|gobuster|wfuzz|nuclei|wp-scan|dotdotpwn)"
^ .* ".*" .* "-"
ignoreregex = ^ .* "GET \/favicon\.ico"
^ .* "GET \/robots\.txt"
Note: The second failregex line catches completely empty User-Agent strings ("-"). Real browsers always send a UA string. If your monitoring stack sends requests without a UA, add its IP to ignoreip in jail.local.
Create a Custom Filter: Brute-Force and Auth Failures
This filter targets HTTP 401 and 403 responses — signs of credential stuffing against wp-login, phpMyAdmin, or any basic-auth-protected path — plus rapid 404 bursts typical of directory enumeration.
sudo nano /etc/fail2ban/filter.d/nginx-auth.local
[Definition]
failregex = ^ .* "(GET|POST) .* HTTP.*" (401|403) .*$
ignoreregex = ^ .* "GET \/\.well-known\/
sudo nano /etc/fail2ban/filter.d/nginx-404.local
[Definition]
failregex = ^ .* "(GET|POST|HEAD) .* HTTP.*" 404 .*$
ignoreregex = ^ .* "(GET|POST) .*\.(css|js|png|jpg|jpeg|gif|ico|woff2?) .*" 404
The ignoreregex in the 404 filter suppresses missing static asset noise — most sites have a handful of these legitimately.
Configure jail.local
If it doesn't exist yet, copy the default:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Open it and set the [DEFAULT] section, then add your nginx jails at the bottom. The key tuning parameters are findtime (sliding window), maxretry (failures within that window), and bantime (how long the ban lasts).
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# IPs that are never banned — add your own static IPs here
ignoreip = 127.0.0.1/8 ::1
# Exponential ban time: first ban = bantime, each repeat multiplies by bantime_factor
bantime = 1h
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 4w
findtime = 10m
maxretry = 5
# Use nftables backend if available; fall back to iptables
backend = systemd
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 1
bantime = 24h
findtime = 1h
[nginx-auth]
enabled = true
port = http,https
filter = nginx-auth
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 1h
findtime = 5m
[nginx-404]
enabled = true
port = http,https
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 20
bantime = 30m
findtime = 2m
Tuning rationale: Bad bots get maxretry = 1 — a single matching request is enough. Brute-force auth gets a tight 5-minute window. Directory scanners fire fast, so a 2-minute findtime with 20 retries catches automated tools without banning users who reload aggressively. The bantime.increment feature (fail2ban 0.11+) means repeat offenders get progressively longer bans up to 4 weeks.
Choose Your Firewall Backend
fail2ban needs to know how to insert ban rules. By default on modern systems it auto-detects, but be explicit to avoid surprises.
ufw (Ubuntu default)
# In [DEFAULT] in jail.local:
banaction = ufw
firewalld (Fedora / RHEL / Rocky)
# In [DEFAULT] in jail.local:
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules
nftables (Arch, or any distro where nftables is primary)
# In [DEFAULT] in jail.local:
banaction = nftables-multiport
banaction_allports = nftables-allports
After editing, reload:
sudo systemctl restart fail2ban
Verify Filters Match Correctly
Before going live, test each filter against your actual log file. fail2ban ships fail2ban-regex for exactly this.
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-badbots.local --print-all-matched
Output will show match counts and the matched lines. If you get zero matches on a log you know contains scanner traffic, the log format may differ from the default combined format. Check with:
tail -5 /var/log/nginx/access.log
If nginx uses a custom log format (e.g., JSON logging), you'll need to adjust failregex to match. The <HOST> token matches any IPv4 or IPv6 address at the position you specify.
Check Active Bans
sudo fail2ban-client status nginx-badbots
sudo fail2ban-client status nginx-auth
sudo fail2ban-client status nginx-404
Output (will vary) looks like:
Status for the jail: nginx-badbots
|- Filter
| |- Currently failed: 2
| `- Total failed: 47
`- Actions
|- Currently banned: 3
`- Total banned: 19
To unban an IP you've accidentally banned:
sudo fail2ban-client set nginx-auth unbanip 203.0.113.42
Troubleshooting
fail2ban isn't banning anything
- Run
sudo fail2ban-client status— if jails show as inactive, there's a config parse error. Checksudo journalctl -u fail2ban -n 50. - Confirm
logpathin your jail matches the real log path. On some setups nginx logs to/var/log/nginx/error.logwith a hostname prefix. - Verify the log file is readable by root:
sudo ls -l /var/log/nginx/access.log.
Legitimate users getting banned
- Add their IP ranges to
ignoreipin[DEFAULT]. - Raise
maxretryfor the offending jail or widenfindtime. - Review the 404 filter's
ignoreregex— add patterns for any missing assets your app legitimately serves with 404.
banaction errors in the log
If you see ERROR Failed to execute ban, the backend doesn't match your firewall. Confirm your active firewall: sudo ufw status, sudo firewall-cmd --state, or sudo nft list ruleset, then set banaction accordingly.
fail2ban-regex shows no matches despite scanner traffic in the log
Your nginx log format differs from combined. Add datepattern and adjust failregex anchors to match your actual line format. The fail2ban docs list all available <F-*> tokens for custom parsing.
Frequently asked questions
- Will fail2ban affect IPv6 traffic?
- Yes, fail2ban's <HOST> token matches both IPv4 and IPv6 addresses, and modern firewall backends (nftables, firewalld) handle IPv6 bans natively. Ensure your nginx log actually records IPv6 addresses — some reverse-proxy setups log only the proxy IP.
- How do I avoid banning Googlebot or other legitimate crawlers?
- Add known crawler IP ranges to ignoreip in jail.local, or add their User-Agent patterns to the ignoreregex in nginx-badbots.local. Note that User-Agent strings can be spoofed; IP-based ignoreip is more reliable for crawlers you've verified.
- Does bantime.increment require a specific fail2ban version?
- Yes — bantime.increment was introduced in fail2ban 0.11. Ubuntu 22.04 ships 0.11.2, Fedora 38+ ships 1.0.x, and Arch tracks the latest release. Ubuntu 20.04 ships 0.11.1 which also supports it. Check with fail2ban-server --version.
- My nginx uses JSON logging. Do I need to change the filters?
- Yes. JSON log lines don't match the combined log format regexes. You'll need to rewrite failregex to match the JSON field layout, using fail2ban's <HOST> token at the position where the client IP appears in your JSON output.
- Can I get email alerts when an IP is banned?
- Yes. In jail.local set action = %(action_mwl)s to send email with whois data and log lines. You'll also need a working MTA (like postfix) and mwhois installed. For lighter-weight alerting, the action_mw variant skips the log-line attachment.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.