$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

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.log and /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. Check sudo journalctl -u fail2ban -n 50.
  • Confirm logpath in your jail matches the real log path. On some setups nginx logs to /var/log/nginx/error.log with 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 ignoreip in [DEFAULT].
  • Raise maxretry for the offending jail or widen findtime.
  • 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.

tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

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