
Bots don’t need a login form to make your VPS miserable. They hammer /wp-login.php, poke /xmlrpc.php, and sweep admin URLs. That keeps your Nginx access logs busy, and the wasted CPU and bandwidth adds up fast.
This walkthrough shows you how to set up Fail2ban for Nginx on an Ubuntu VPS using your real Nginx logs. The defaults are effective, and they’re unlikely to lock you out of SSH.
You’ll create a small set of Fail2ban jails, confirm bans end-to-end, and avoid two common “Fail2ban doesn’t work” issues:
- Reading the wrong log source
- Banning the wrong IP because you’re behind Cloudflare
Prerequisites and what this setup protects
- An Ubuntu VPS you control (Ubuntu 22.04 LTS or Ubuntu 24.04 LTS). Root access or a sudo user.
- Nginx installed and serving at least one site.
- Basic firewall access (UFW or raw iptables/nftables is fine).
This guide sticks to Nginx log-driven bans. It targets common, high-volume abuse patterns:
- Brute-force attempts against WordPress login paths
- XML-RPC abuse (still common in 2026)
- High-rate request floods that look like “scraping” or “credential stuffing”
- Bad bots that rack up 404s across your site map
In production, you want predictable performance and full control over Nginx, firewall rules, and logging.
A HostMyCode VPS fits that model. You can tune the whole stack without shared-host constraints.
Step 1: Confirm Nginx is logging where Fail2ban can read
Fail2ban can only act on what it can parse. Start by confirming where Nginx writes access logs.
sudo nginx -T 2>/dev/null | grep -R "access_log" -n --color=never || true
ls -lah /var/log/nginx/
On most Ubuntu installs you’ll see:
/var/log/nginx/access.log/var/log/nginx/error.log
Make a request and confirm the log updates:
curl -I http://127.0.0.1/ 2>/dev/null | head -n 1
sudo tail -n 5 /var/log/nginx/access.log
Common mistake: some hardened Nginx setups disable access logs. Others send them to syslog/journald only.
If /var/log/nginx/access.log doesn’t show fresh entries, fix logging first. Otherwise, point Fail2ban at the correct log path.
Step 2: Install Fail2ban and enable it
Install Fail2ban from Ubuntu’s repositories. That’s fine for production use in 2026.
Then enable and start the service:
sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
Sanity check the daemon:
sudo fail2ban-client ping
You should see PONG.
Step 3: Make sure you won’t lock yourself out (SSH + ignoreip)
Do the safety work before you touch any Nginx jails:
- Whitelist your own IP (or your office/VPN subnet)
- Keep SSH protected, but avoid brittle settings
Create /etc/fail2ban/jail.local. Fail2ban loads jail.conf first. Then it applies your overrides from jail.local.
sudo nano /etc/fail2ban/jail.local
Paste this baseline and replace 203.0.113.10 with your real public IP (or a subnet like /32, /29, etc.). If your IP changes often, leave it out. Just be careful during testing.
[DEFAULT]
# Ban for 1 hour (tune later)
bantime = 1h
# Look back 10 minutes for repeated failures
findtime = 10m
# Ban after 10 matches in findtime
maxretry = 10
# Whitelist your IP(s) to avoid accidental self-bans
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
# Use systemd backend on Ubuntu (works well with log rotation)
backend = systemd
# Ban action: use nftables if available; iptables as fallback
banaction = nftables-multiport
# Email alerts are optional; keep it quiet unless you really need noise
# destemail = you@example.com
# sender = fail2ban@your-hostname
# mta = sendmail
# action = %(action_)s
[sshd]
enabled = true
port = ssh
maxretry = 6
findtime = 10m
bantime = 2h
Restart Fail2ban and confirm it loads cleanly:
sudo systemctl restart fail2ban
sudo fail2ban-client status
Make sure sshd appears in the jail list.
If you want another easy win on the same server, add automatic security updates. It’s a separate layer from Fail2ban, but they pair well: set up automated security updates on Ubuntu VPS.
Step 4: Create Nginx filters for real-world abuse
Fail2ban matches patterns (regex) in a log file. When an IP hits a pattern too many times within a window, Fail2ban adds a firewall rule.
Ubuntu often ships a few Nginx filters. In practice, they don’t always match your log format or your traffic.
The filters below stay narrow and practical.
Create a filter for WordPress login abuse:
sudo nano /etc/fail2ban/filter.d/nginx-wp-login.conf
[Definition]
failregex = ^<HOST> - .* "(GET|POST) \/wp-login\.php(\?.*)? HTTP\/.*" (200|401|403|404) .*$
^<HOST> - .* "POST \/wp-login\.php HTTP\/.*" (200|401|403|404) .*$
ignoreregex =
Create a filter for XML-RPC abuse:
sudo nano /etc/fail2ban/filter.d/nginx-wp-xmlrpc.conf
[Definition]
failregex = ^<HOST> - .* "POST \/xmlrpc\.php HTTP\/.*" (200|301|302|401|403|404) .*$
ignoreregex =
Create a filter for excessive 404 scanning (useful for broad bot probing):
sudo nano /etc/fail2ban/filter.d/nginx-404-scan.conf
[Definition]
failregex = ^<HOST> - .* "(GET|POST|HEAD) .* HTTP\/.*" 404 .*$
ignoreregex =
Why include “successful” status codes? Abuse traffic doesn’t reliably produce 401/403.
Bots often treat 404 as recon feedback. Repeated 200s to wp-login.php or xmlrpc.php can still be hostile.
Step 5: Add Nginx jails (with safe thresholds)
Now connect those filters to jails. Append the following blocks to /etc/fail2ban/jail.local.
sudo nano /etc/fail2ban/jail.local
[nginx-wp-login]
enabled = true
port = http,https
filter = nginx-wp-login
# Adjust if your access log is elsewhere
logpath = /var/log/nginx/access.log
# Typical brute force is fast; keep the window short
findtime = 5m
maxretry = 8
bantime = 6h
[nginx-wp-xmlrpc]
enabled = true
port = http,https
filter = nginx-wp-xmlrpc
logpath = /var/log/nginx/access.log
findtime = 10m
maxretry = 6
bantime = 12h
[nginx-404-scan]
enabled = true
port = http,https
filter = nginx-404-scan
logpath = /var/log/nginx/access.log
# 404s happen naturally; don’t ban on light noise
findtime = 2m
maxretry = 60
bantime = 2h
Restart Fail2ban and verify each jail is loaded:
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status nginx-wp-login
sudo fail2ban-client status nginx-wp-xmlrpc
sudo fail2ban-client status nginx-404-scan
Step 6: Verify your filters against real logs (before you trust them)
Use fail2ban-regex to test each filter against your current access log. This removes guesswork and shows exactly what matches.
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-login.conf --print-all-matched
If you run WordPress, you may see matches from your own legitimate logins. That’s expected.
The jail triggers on repetition, not on a single request.
Test the XML-RPC filter:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-xmlrpc.conf --print-all-matched
And the 404 scan filter:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-404-scan.conf --print-all-matched
If you get 0 matches, stop and validate the basics:
- Are you using a custom
log_formatthat changes field order or drops the client IP? - Is the site behind a reverse proxy/CDN so Nginx logs the proxy IP instead of the visitor?
- Is
logpathpointing at the right vhost log file?
Step 7: Handle the Cloudflare (or proxy) IP problem correctly
If your domain sits behind Cloudflare, your access logs often show Cloudflare edge IPs as the “client.” If you feed that into Fail2ban, you can end up banning Cloudflare. That can take your site offline for everyone.
The fix is straightforward. Configure Nginx to restore the real client IP using real_ip.
Then make sure your log format uses $remote_addr. After Nginx rewrites it, $remote_addr becomes the real IP.
On Ubuntu, create a small include file:
sudo nano /etc/nginx/conf.d/realip-cloudflare.conf
Add Cloudflare’s IP ranges (example list trimmed). In production, use Cloudflare’s current published ranges and keep them updated.
# Trust Cloudflare to set the real client IP
real_ip_header CF-Connecting-IP;
# Cloudflare IPv4 (sample; add full list)
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;
# Cloudflare IPv6 (sample; add full list)
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
Reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Browse the site and confirm the access log now shows your real public IP.
If it doesn’t, your log format may be logging $http_x_forwarded_for (or something similar) instead. Keep the logging simple and consistent.
If you’re changing DNS or proxy settings at the same time, plan the cutover carefully. This related guide helps: migrate a domain to Cloudflare DNS without downtime.
Step 8: Trigger a test ban (safely) and confirm it’s enforced
Testing from your own IP is only safe if you configured ignoreip. If you didn’t whitelist yourself, test from a different network.
If you can’t do either, skip this step until you can.
Generate repeated requests to /wp-login.php to exceed maxretry. Replace example.com with your domain.
for i in $(seq 1 12); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php; done
Check the jail status and banned list:
sudo fail2ban-client status nginx-wp-login
Then confirm the block exists at the firewall layer. With nftables:
sudo nft list ruleset | sed -n '1,200p'
If you’re using iptables, you’ll typically see an f2b-* chain:
sudo iptables -S | head
sudo iptables -L -n | sed -n '1,120p'
What “working” looks like: the IP shows up under Banned IP list for that jail. Requests from that IP fail (often as a timeout or a blocked connection).
Step 9: Unban an IP and tune thresholds based on your traffic
You’ll occasionally catch a legitimate user behind shared NAT. This is most common with the 404 jail.
Unban them like this:
sudo fail2ban-client set nginx-404-scan unbanip 203.0.113.55
Tuning changes that usually reduce false positives:
- If your site generates lots of legitimate 404s (old URLs, broken links), raise
maxretryfornginx-404-scanor disable it. - If bots rotate IPs slowly, increase
findtimefor the WordPress jails so Fail2ban can “see” the pattern. - If you want longer bans without going permanent, use a ramp: start at 1–6 hours and extend if needed.
Also watch the bigger picture. If the application is struggling under load, banning noise only helps so much.
For WordPress, caching often reduces load more than any ban policy. Redis object cache is a solid next step: Install Redis Object Cache for WordPress on Ubuntu.
Step 10: Make Fail2ban survive log rotation and restarts
On Ubuntu, Nginx logs rotate via logrotate, usually daily. Fail2ban with the systemd backend generally handles rotation well.
That assumes it’s reading the right source.
Confirm the Nginx logrotate config exists:
sudo cat /etc/logrotate.d/nginx
Force a test rotation (this rotates logs; it doesn’t delete them):
sudo logrotate -f /etc/logrotate.d/nginx
sudo systemctl reload nginx
Make a few requests. Then check jail counters again to confirm Fail2ban is still tracking events.
Step 11: Add a lightweight “abuse dashboard” using Fail2ban logs
Fail2ban logs to /var/log/fail2ban.log. Skim it to see what’s being banned and which jail triggered it:
sudo tail -n 80 /var/log/fail2ban.log
sudo grep -E "Ban|Unban" /var/log/fail2ban.log | tail -n 50
To get a quick view of repeat request sources in Nginx logs:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 20
If Nginx is throwing 502s under load or after upstream changes, fix that first. Fail2ban won’t address upstream failures.
This guide helps: Nginx 502 Bad Gateway fix on Ubuntu VPS.
Common mistakes (and quick fixes)
- Banning Cloudflare: Fix real client IP handling in Nginx (Step 7). Then restart Fail2ban and clear bad bans.
- Fail2ban shows jails, but never bans: Your
logpathis wrong or your log format doesn’t match the filter. Usefail2ban-regexto prove it. - You’re banning yourself: Add your stable IP to
ignoreip, restart Fail2ban, then unban. - High CPU usage from Fail2ban: Keep filters narrow and avoid huge logpaths when you can. Consider splitting vhost logs per site so jails scan smaller files.
- Thinking Fail2ban replaces WAF/rate limiting: It doesn’t. It’s a pragmatic layer that blocks repeat offenders based on logs.
Summary: a production-safe baseline for Nginx bans
You now have Fail2ban reading Nginx logs and banning repeated abuse of WordPress endpoints. You also have coverage for noisy scanners.
Two details decide whether this works in production. The log file must contain the events you care about. The IP in the log must be the real client.
If you want this running on infrastructure you can tune and grow, start with a HostMyCode VPS. If you’d rather offload patching, monitoring basics, and production-safe changes, managed VPS hosting is the calmer route.
If bot abuse is burning resources, Fail2ban works best on a VPS where you control Nginx, logging, and firewall rules. HostMyCode offers a flexible HostMyCode VPS, plus a hands-off option with managed VPS hosting for production sites.
FAQ
Should I run Fail2ban on a web server behind Cloudflare?
Yes, but only after Nginx logs the real client IP. Configure real_ip_header CF-Connecting-IP and trust Cloudflare IP ranges, otherwise you’ll ban Cloudflare edges.
What ban times work well for website bots?
Start with 1–6 hours for login abuse and 1–2 hours for 404 scanning. If abuse persists from the same IPs, extend to 12–24 hours. Permanent bans can backfire because IP ownership changes.
Does Fail2ban replace a firewall or WAF?
No. Fail2ban triggers firewall rules based on observed log patterns. It’s best used alongside a basic firewall policy (only open needed ports) and sane Nginx limits.
How do I quickly see what’s being banned?
Check /var/log/fail2ban.log, and run sudo fail2ban-client status <jailname>. That shows current bans and the filter’s match count.
Can this break legitimate traffic?
It can if your thresholds are too low or your filters are too broad. The 404 jail is the usual culprit—raise maxretry or disable it if your site has lots of old links.