If you run Nginx on a VPS, your access logs are full of brute force attempts, credential stuffing bots, and scanners probing for vulnerabilities. Fail2ban watches those logs and temporarily bans IPs that cross a threshold. It’s one of the simplest, most effective tools for keeping bad actors off your server.

TL;DR

  • Fail2ban monitors Nginx logs and auto-bans IPs that trigger too many errors (401s, 403s, 404s)
  • Built-in filters cover HTTP auth and bot scanning. Custom filters handle rate limiting, SSL probes, and WordPress attacks
  • The recidive jail catches repeat offenders and escalates bans from hours to weeks
  • Always test filters with fail2ban-regex before enabling a jail. A bad regex bans legitimate traffic

When Fail2ban Is Worth It

Fail2ban shines on web servers where password-based authentication is exposed to the internet. SSH with key-only auth doesn’t need it (there’s nothing to brute force). But Nginx serving WordPress login pages, HTTP basic auth endpoints, or any application with a login form? That’s where fail2ban earns its keep.

It’s not a firewall replacement. It’s a reactive layer that reads log patterns and issues temporary bans via iptables (or nftables on Ubuntu 22.04+). Think of it as a bouncer who watches for repeat troublemakers and throws them out.

Use fail2ban when:

  • Your Nginx server has login pages exposed to the public internet
  • You see brute force attempts in access/error logs
  • You run WordPress, phpMyAdmin, or other apps with web-based authentication
  • You want to reduce noise from scanners and bots hitting non-existent paths

Skip it when:

  • You’re on shared hosting (no root access, hosting provider handles this)
  • SSH is key-only and there’s no web login to protect
  • All traffic routes through Cloudflare with their bot management enabled (though fail2ban can still catch what leaks through)

Installation and Base Config

On Ubuntu/Debian:

sudo apt update && sudo apt install fail2ban -y

Fail2ban reads its config from /etc/fail2ban/jail.conf, but you should never edit that file directly. It gets overwritten on updates. Create a local override:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Open /etc/fail2ban/jail.local and set your defaults in the [DEFAULT] section:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 YOUR_HOME_IP
backend  = systemd
banaction = nftables-multiport

Key settings explained:

  • bantime — how long an IP stays banned (1 hour is a good starting point)
  • findtime — the window to count failures (5 failures in 10 minutes triggers a ban)
  • maxretry — failures allowed before banning
  • ignoreip — IPs that never get banned. Always include your own IP here
  • banaction — use nftables-multiport on Ubuntu 22.04+ (nftables replaced iptables). On older systems, use iptables-multiport

Always set ignoreip. Locking yourself out of your own server is the most common fail2ban mistake. Add your home IP, office IP, and any monitoring service IPs.

Built-in Nginx Jails

Fail2ban ships with two Nginx filters ready to use. Add these to /etc/fail2ban/jail.local:

nginx-http-auth

Catches failed HTTP Basic Authentication attempts logged to Nginx’s error log.

[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log
maxretry = 3

nginx-botsearch

Bans IPs that repeatedly hit non-existent paths (phpMyAdmin, wp-admin on non-WordPress sites, .env files, etc.). These are almost always automated scanners.

[nginx-botsearch]
enabled  = true
port     = http,https
filter   = nginx-botsearch
logpath  = /var/log/nginx/access.log
maxretry = 5

Custom Filters for Common Attacks

The built-in filters cover the basics. For real-world Nginx deployments, you’ll want custom filters for the attack patterns that actually fill your logs.

Ban Excessive 4xx Errors

Scanners and exploit kits generate floods of 400, 403, and 404 responses. Create /etc/fail2ban/filter.d/nginx-4xx.conf:

[Definition]
failregex = ^ .* "(GET|POST|HEAD).*" (400|403|404|405|408) .*$
ignoreregex = \.(jpg|jpeg|png|gif|css|js|ico|svg|woff2?|ttf)

The ignoreregex line prevents banning visitors who just hit a missing image or font file. Add the jail:

[nginx-4xx]
enabled  = true
port     = http,https
filter   = nginx-4xx
logpath  = /var/log/nginx/access.log
maxretry = 10
findtime = 1m

Ban Nginx Rate Limit Violators

If you use Nginx’s limit_req directive for rate limiting, fail2ban can escalate those temporary 429s into IP bans. Nginx logs rate limit violations to its error log. Create /etc/fail2ban/filter.d/nginx-limit-req.conf:

[Definition]
failregex = limiting requests, excess: .* by zone .*, client: 
ignoreregex =
[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 5
findtime = 5m
bantime  = 2h

Ban SSL Protocol Errors

Bots probing your server with bad SSL handshakes or outdated protocols show up in the error log. Create /etc/fail2ban/filter.d/nginx-sslerror.conf:

[Definition]
failregex = SSL_do_handshake\(\) failed .* client: 
            no "ssl_certificate" is defined .* client: 
ignoreregex =
[nginx-sslerror]
enabled  = true
port     = http,https
filter   = nginx-sslerror
logpath  = /var/log/nginx/error.log
maxretry = 3
bantime  = 1d

Protect Nginx WordPress with Fail2ban

WordPress is a special case. By default, WordPress returns HTTP 200 on failed login attempts. That’s a problem because fail2ban watches for error codes (401, 403) in logs. A 200 response looks like a success, so brute force attempts go undetected.

The fix is a mu-plugin that returns HTTP 403 on failed logins. Create wp-content/mu-plugins/fail2ban-403.php:

<?php
add_action('wp_login_failed', function() {
    status_header(403);
});

Now failed WordPress logins generate a 403 in your Nginx access log, and fail2ban can catch them. Create /etc/fail2ban/filter.d/nginx-wordpress.conf:

[Definition]
failregex = ^ .* "POST /wp-login\.php.*" 403 .*$
            ^ .* "POST /xmlrpc\.php.*" (403|500) .*$
ignoreregex =
[nginx-wordpress]
enabled  = true
port     = http,https
filter   = nginx-wordpress
logpath  = /var/log/nginx/access.log
maxretry = 3
findtime = 5m
bantime  = 1d

This catches two attack vectors: brute force on the login page and XML-RPC abuse (which bots use for amplification attacks and credential stuffing). If you don’t use XML-RPC (most modern WordPress sites don’t), you can also block it entirely in your Nginx server config:

location = /xmlrpc.php {
    deny all;
    return 403;
}

The Recidive Jail

Here’s a pattern most fail2ban guides skip: an IP gets banned for an hour, the ban expires, and the same IP immediately starts attacking again. The recidive jail solves this by monitoring fail2ban’s own log. If an IP gets banned 3+ times across any jail, it gets a much longer ban.

[recidive]
enabled  = true
filter   = recidive
logpath  = /var/log/fail2ban.log
bantime  = 1w
findtime = 1d
maxretry = 3

With this config, an IP that gets banned 3 times in 24 hours gets blocked for a full week. This is one of the most effective jails you can run because it catches persistent attackers who rotate through different attack vectors. The recidive filter ships with fail2ban, so no custom filter file needed.

Testing Your Filters

Never enable a jail without testing the regex first. A bad filter pattern can match legitimate traffic and lock out real users. Use fail2ban-regex to test against your actual log files:

# Test a filter against your access log
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-4xx.conf

# Test against your error log
fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-limit-req.conf

The output shows how many lines matched, how many failed to match, and exactly which lines triggered the regex. You want matches on actual attack traffic and zero matches on legitimate requests.

After enabling jails, check their status:

# List all active jails
sudo fail2ban-client status

# Check a specific jail
sudo fail2ban-client status nginx-wordpress

# Unban an IP manually (if you locked yourself out)
sudo fail2ban-client set nginx-wordpress unbanip 203.0.113.50

Cloudflare and CDN Considerations

If your site sits behind Cloudflare or another CDN, every request in your Nginx log comes from a Cloudflare IP, not the visitor’s real IP. Banning Cloudflare’s IP blocks all traffic through that node.

Fix this by configuring Nginx to use the real client IP from the CF-Connecting-IP header (Cloudflare) or X-Forwarded-For (generic CDN). In your Nginx config:

# For Cloudflare — add to nginx.conf or site config
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
real_ip_header CF-Connecting-IP;

After this change, Nginx logs the visitor’s real IP, and fail2ban bans the actual attacker instead of Cloudflare’s proxy. Keep the Cloudflare IP ranges updated. They publish the current list at cloudflare.com/ips.

Frequently Asked Questions

Does fail2ban replace a firewall?

No. Fail2ban is a reactive tool that bans IPs after they misbehave. A firewall (UFW, nftables) sets proactive rules about which ports and protocols are allowed. Use both together.

What happens when a banned IP tries to connect?

Fail2ban adds a drop rule to nftables (or iptables). The connection is silently dropped at the network level before it reaches Nginx. The attacker sees a timeout, not an error page.

Can fail2ban ban IPv6 addresses?

Yes, as of fail2ban 0.10+. Make sure your banaction supports IPv6 (nftables-multiport does). If you use the older iptables-multiport, you need a separate banaction_allports = ip6tables-allports line.

How do I permanently ban an IP?

Set bantime = -1 in the jail config. But permanent bans fill your nftables rules over time. A better approach is the recidive jail with a long bantime (1 week or 1 month) combined with a firewall blocklist for known bad ranges.

Will fail2ban slow down my server?

Minimal impact. Fail2ban reads log files with inotify (no polling) and only runs regex when new lines appear. On a VPS with normal traffic, the CPU overhead is negligible.

Summary

Fail2ban is one of those tools that takes 20 minutes to set up and quietly protects your server for years. Start with the two built-in Nginx jails (HTTP auth and botsearch), add custom filters for your specific traffic patterns, and always enable the recidive jail for repeat offenders. Test every filter with fail2ban-regex before going live, and keep your own IP in ignoreip.

If you’re running WordPress on Nginx, the mu-plugin trick to return 403 on failed logins is essential. Without it, fail2ban can’t see brute force attempts at all. Combined with proper VPS hardening and bot traffic management, fail2ban rounds out a solid defense stack that handles most threats without any ongoing maintenance.

For sites that need managed security without the server admin work, Cloudways and Kinsta both include server-level firewalls and brute force protection out of the box.