WordPress Brute Force Protection with fail2ban: Jail, Filter and Testing
Bots attempting thousands of logins daily on your WordPress admin panel consume server resources and slow down your site. How to set up automatic IP banning with fail2ban?

Last month, I noticed a strange slowdown on a client's WordPress site. The server CPU was hovering around 80%, but traffic was even lower than normal. When I checked the Nginx logs and saw over 14,000 wp-login.php requests per day, the situation became clear: bots were brute force attacking the admin panel. Cloudflare's rate limiting was in place but wasn't enough, because the attackers were constantly changing their IPs. That day I installed fail2ban and within 3 hours, CPU usage dropped to 12%. Now I'll explain the same setup step by step, because it's as important as a firewall for WordPress security, but very few people configure it correctly.
fail2ban monitors failed login attempts to your server and automatically bans IP addresses when a certain threshold is exceeded. It reads logs from services like SSH, FTP, Apache, Nginx and takes action based on patterns. By writing custom jail and filter files for WordPress, you can protect target points like wp-login.php and xmlrpc.php. I install it by default on our servers in the Netherlands, because manual IP banning is both time-consuming and you're always too late. Especially on high-traffic sites (doktorbul.com has 79,000 profile pages), protecting server resources is critical.
In this article, we'll cover the basic logic of fail2ban, custom jail and filter files for WordPress, testing methods, and real case results. It requires technical knowledge, but I'll explain every step with terminal commands. Goal: 1-hour setup, zero manual intervention afterwards.
How fail2ban Works: The Jail, Filter and Action Triangle
fail2ban consists of three main components: jail (prison), filter (sieve), and action. Jail defines which service you'll monitor. Filter contains the regex patterns to search for in log files. Action determines what happens when the threshold is exceeded (usually IP banning via iptables).
Example scenario: Someone enters the wrong password 10 times in 5 minutes on wp-login.php. fail2ban reads the Nginx access.log, catches the "POST /wp-login.php" and "HTTP 200" pattern with the regex in the filter file, when the counter reaches 10 it sends an "ban this IP for 1 hour" command to iptables. The user gets "403 Forbidden" on the next request, and your server saves resources by not processing requests from that IP.
Installation on Debian/Ubuntu:
sudo apt update
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
The default config file is /etc/fail2ban/jail.conf but we don't edit it directly. Instead, we create /etc/fail2ban/jail.local and override it, because jail.conf can be reset during package updates. I always use jail.local, so my custom settings are preserved.
Basic parameters:
- bantime: How long the IP will remain banned (example: 3600 = 1 hour)
- findtime: Within how many seconds to check for maxretry count (example: 600 = 10 minutes)
- maxretry: How many failed attempts within findtime trigger a ban (example: 5)
Example: bantime=3600, findtime=600, maxretry=5 means "ban IPs that make 5 failed attempts in 10 minutes for 1 hour". I generally use maxretry=3 for WordPress, because legitimate users don't enter wrong passwords 3 times, bots do.
Creating a Custom Jail File for WordPress
Create the /etc/fail2ban/jail.local file and add this content:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
destemail = info@futia.net
sender = fail2ban@futia.io
action = %(action_mwl)s
[wordpress-auth]
enabled = true
filter = wordpress-auth
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 7200
findtime = 600
port = http,https
[wordpress-xmlrpc]
enabled = true
filter = wordpress-xmlrpc
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 86400
findtime = 300
port = http,https
Here we've defined two jails:
1. wordpress-auth: Catches brute force attempts on wp-login.php. 3 failed logins in 10 minutes = 2 hour ban. 2. wordpress-xmlrpc: Catches spam and DDoS attempts on xmlrpc.php. 2 requests in 5 minutes = 24 hour ban (because xmlrpc is generally used by bots).
Adjust the logpath section according to your server. If you're using Apache, it might be /var/log/apache2/access.log. I prefer Nginx because it's more performant under high traffic. At diolivo.com.tr we experienced 340% traffic growth in 6 months, the server could have crashed without Nginx.
How to Find Logpath
If you don't know where your log file is:
sudo nginx -V 2>&1 | grep "access-log"
# or
sudo find /var/log -name "*access.log"
Important for WordPress: If you're using Cloudflare, Nginx logs show Cloudflare IPs instead of real IPs. To fix this, ngx_http_realip_module must be active and add this to nginx.conf:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# (other Cloudflare IP blocks)
real_ip_header CF-Connecting-IP;
If I don't do this at futia.io, fail2ban bans Cloudflare's IP and the entire site crashes. So definitely test this during initial setup.
Filter Files: Scanning Logs with Regex Patterns
fail2ban uses regex to read log files. Filter files are located in the /etc/fail2ban/filter.d/ folder. We'll create two custom filters for WordPress.
wordpress-auth.conf
Create the /etc/fail2ban/filter.d/wordpress-auth.conf file:
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php HTTP.*" 200
^<HOST> .* "POST /wp-admin/admin-ajax\.php HTTP.*" 200
^<HOST> .* "POST /xmlrpc\.php HTTP.*" 200
ignoreregex =
This regex searches for this format in Nginx access.log:
192.168.1.50 - - [15/May/2024:14:32:10 +0000] "POST /wp-login.php HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
The <HOST> part is fail2ban's special variable, it automatically captures the IP address. The 200 status code is important, because even failed login attempts return 200 (WordPress's poor design). If it returned 401 or 403, it would be easier to catch.
wordpress-xmlrpc.conf
Create the /etc/fail2ban/filter.d/wordpress-xmlrpc.conf file:
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php HTTP.*"
ignoreregex =
xmlrpc.php is WordPress's legacy API system. It's heavily used for pingback spam, brute force and DDoS. I tell clients "disable xmlrpc completely", but some use it for mobile apps. So if you can't disable it, protect it tightly with fail2ban.
Testing Filters
To test whether your filter works, use fail2ban-regex:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf
You'll see "Success" in the output and the number of lines caught. If you see 0 matches, either the regex is wrong or the log format is different. I always test whenever I write a new filter, otherwise I spend 2 hours in production searching for errors wondering "why isn't it banning".
Restarting fail2ban Service and Status Check
After adding new jails and filters, restart the service:
sudo systemctl restart fail2ban
sudo systemctl status fail2ban
If there are errors, check the log file:
sudo tail -f /var/log/fail2ban.log
Common errors:
- "No file(s) found for glob": logpath is wrong or file doesn't exist
- "Unable to find a corresponding IP address": regex can't capture IP, check
<HOST>placement - "Already running": Service is already running, do
sudo systemctl stop fail2banand restart
To see active jails:
sudo fail2ban-client status
Output:
Status
|- Number of jail: 2
`- Jail list: wordpress-auth, wordpress-xmlrpc
To see details of a specific jail:
sudo fail2ban-client status wordpress-auth
In the output, you'll see banned IPs in the "Currently banned" section. I check this once a day to see if any legitimate users were accidentally banned.
Manual Testing: Banning and Unbanning Your Own IP
To make sure the setup works, deliberately ban your own IP. WARNING: Before doing this, make sure your SSH connection won't drop, or you'll lose access to your server.
1. Find your current IP:
curl ifconfig.me
2. Enter wrong password 3 times on WordPress login page (from browser or with curl):
curl -X POST https://yoursite.com/wp-login.php \
-d "log=admin&pwd=wrong_password&wp-submit=Log+In"
Repeat this 3 times. Then check the fail2ban log:
sudo tail -f /var/log/fail2ban.log
You should see a line like this:
2024-05-15 14:35:22,341 fail2ban.actions [12345]: NOTICE [wordpress-auth] Ban 192.168.1.50
3. To manually remove your IP:
sudo fail2ban-client set wordpress-auth unbanip 192.168.1.50
Or to clear all banned IPs:
sudo fail2ban-client unban --all
I do this frequently in test environments, but be careful in production. If you accidentally ban your own office IP and don't have SSH access, you'll need to log in through your server provider's console.
Real Case: 3,200 Daily Brute Force Attempts on memuratamalari.com
memuratamalari.com is a site that automatically publishes public personnel job postings. We pull 50+ postings daily from the ilan.gov.tr API, with 40,400 monthly organic searches. Last year, 2 days after moving the site to a new server, CPU usage jumped to 90%. I examined the logs, there were over 3,200 wp-login.php requests per day, all from different IPs.
First attempt, I used Wordfence, but the free version can't do rate limiting, it only sends notifications. Premium is $119/year, the client said "solve it at server level". I installed fail2ban, used these settings:
maxretry=2(very aggressive, but bots don't try more than 2 times)bantime=14400(4 hours, long but effective)findtime=300(5 minutes, short window)
Within 3 days, 847 IPs were banned. CPU usage dropped to 15%, page load time went from 2.1 seconds to 0.8 seconds. The client said "we noticed the site got faster", and I said "yes, because we're no longer wasting resources on bots".
Additionally, I completely disabled xmlrpc.php (from WordPress settings), because the site wasn't using a mobile app. If you are using one, protection with fail2ban is sufficient.
Advanced: Email Notifications and Slack Integration
fail2ban can send emails when it bans an IP. The action = %(action_mwl)s line in jail.local activates this. mwl = "mail with log", meaning it sends mail with log lines.
For email settings in /etc/fail2ban/jail.local:
[DEFAULT]
destemail = info@futia.net
sender = fail2ban@futia.io
mta = sendmail
You can also use mail or postfix instead of sendmail. I generally install Postfix because it's more reliable. But I prefer Slack notifications over email, I see them faster.
For Slack integration, create a custom action. /etc/fail2ban/action.d/slack-notify.conf:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d '{"text":"fail2ban: <ip> banned on <name>"}'
actionunban =
Then in jail.local:
[wordpress-auth]
enabled = true
filter = wordpress-auth
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 7200
action = %(action_)s
slack-notify[name=wordpress-auth]
I use this at futia.io. I get a Slack notification every time an IP is banned, so I immediately notice abnormal activity. For example, one day 40 IPs were banned simultaneously, I realized it was a DDoS attack and activated "Under Attack" mode on Cloudflare.
fail2ban Performance and Server Resources
fail2ban is written in Python, it continuously reads log files. On high-traffic sites (1M+ requests per day), if the log file gets too large, fail2ban can slow down. I do these optimizations:
1. Log rotation: Rotate Nginx logs daily, so file size doesn't exceed 100MB. 2. usedns = no: Add usedns = no in jail.local, doesn't do DNS lookup, faster. 3. dbfile: Move fail2ban database to tmpfs (runs in RAM).
sudo mkdir -p /var/run/fail2ban
sudo chown fail2ban:fail2ban /var/run/fail2ban
Then in /etc/fail2ban/fail2ban.local:
[Definition]
dbfile = /var/run/fail2ban/fail2ban.sqlite3
This reduces disk I/O, especially noticeable on servers without SSDs. doktorbul.com has 79,000 profile pages, receiving 200,000+ requests per day, fail2ban uses 0.3% of CPU. So we're not experiencing performance issues.
Common Errors and Solutions
1. fail2ban Not Banning IPs
- Filter regex is wrong: test with
fail2ban-regex - Log file path is wrong: check
logpath - Service not running:
sudo systemctl status fail2ban
2. Your Own IP Got Banned
Add to whitelist. In jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 YOUR_OFFICE_IP
I always add my office IP and Netherlands server IPs to the whitelist.
3. Cloudflare IPs Getting Banned
If you haven't set real_ip_header CF-Connecting-IP in Nginx, fail2ban sees Cloudflare's IP and bans it. Apply the settings from the "How to Find Logpath" section above.
4. fail2ban Won't Start
Usually there's a syntax error. Check:
sudo fail2ban-client -t
It shows the error line. I usually forget commas or quotes, so I test after every edit.
If your WordPress site is high-traffic or constantly under bot attacks, installing fail2ban takes 1 hour of your time but saves server resources for months. At FUTIA, I install it by default on all client sites, because I don't want them calling later saying "site is slow". For technical support, you can reach me via WhatsApp +90 532 491 17 05 or write to info@futia.net. I work from the Netherlands, I usually respond within 2 hours.
Frequently Asked Questions
Is fail2ban sufficient for WordPress, or do I also need a plugin like Wordfence?
Because fail2ban works at the server level, it's independent of WordPress, faster, and doesn't consume resources. Plugins like Wordfence work at the PHP level, activate on every request, and make database queries. I recommend using both together: fail2ban for brute force, Wordfence for malware scanning and firewall rules. But if budget is limited, fail2ban alone provides 80% protection. At memuratamalari.com we only use fail2ban, with 40,400 monthly organic searches we've had no issues.
Can fail2ban accidentally ban legitimate users?
Yes, if you set maxretry too low (for example 2) and a user forgets their password, they can get banned. I generally use maxretry=3, 3 wrong attempts is reasonable for a normal user. Also, whitelist your office IP or trusted IP blocks with the ignoreip parameter. If accidental banning happens, users can immediately remove it with the 'sudo fail2ban-client set wordpress-auth unbanip IP_ADDRESS' command. I recommend 'bantime=3600' (1 hour) to clients, not too long but deters bots.
How to use fail2ban on WordPress running behind Cloudflare?
If you're using Cloudflare, Nginx logs show Cloudflare IPs instead of real IPs. To fix this, ngx_http_realip_module must be active in Nginx and Cloudflare IP blocks must be added to nginx.conf with 'set_real_ip_from' directives. Also add the 'real_ip_header CF-Connecting-IP;' line. If you don't do this, fail2ban will ban Cloudflare's IP and your entire site will crash. I made this mistake once at futia.io, the site was down for 10 minutes. Since that day, I do the real_ip setup as the first step in every installation.
What's the fastest way to test fail2ban jail and filter files?
The fail2ban-regex command is the fastest test method. Example: 'sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf' command shows how many log lines your filter caught. If you see 0 matches, either the regex is wrong or the log format is different. Also, the 'sudo fail2ban-client -t' command checks for syntax errors. Whenever I write a new filter, I always run these two commands, otherwise I spend hours in production searching for errors wondering 'why isn't it banning'. Deliberately banning and removing your own IP in a test environment is also good practice.
How to block xmlrpc.php attacks with fail2ban?
xmlrpc.php is WordPress's legacy API system, heavily used for pingback spam and brute force. To block it with fail2ban, create a special jail: maxretry=2, bantime=86400 (24 hours), findtime=300 (5 minutes). In the filter file, search for the 'POST /xmlrpc\.php' pattern. But the best solution is to completely disable xmlrpc if you're not using a mobile app. You can disable it from WordPress settings or with a 'deny from all' rule in .htaccess. I disabled xmlrpc at memuratamalari.com, daily 800+ spam attempts dropped to zero. If you can't disable it, fail2ban provides sufficient protection.
Want to apply one of the techniques from this post? Fill out a short form and we'll email you a free preview audit within 48 hours.