fail2ban WordPress Brute Force Protection: Jail + Filter + Test
Are bots making 200 attempts per minute on your WordPress admin panel slowing down your server? 3 lines of configuration with fail2ban, automatic IP banning, and 87% CPU savings.

Last week, CPU usage on kamupersonelhaber.com's server spiked to 92%. The reason: an army of bots from Chinese IPs making 200 attempts per minute, scanning wp-login.php. Cloudflare WAF was enabled but the rate limit settings weren't strict enough. We installed fail2ban in 15 minutes, wrote a custom jail, and CPU dropped to 18%. Using open-source tools like fail2ban instead of writing firewall rules to reduce server costs is both faster and more flexible. In this post, I'll share step-by-step the WordPress-specific jail file, filter regex, and testing process. You can implement it on your own server in 10 minutes.
What is fail2ban and Why It's Critical for WordPress
fail2ban is a Python application that continuously monitors log files and automatically blocks IPs matching specific patterns by adding them to iptables. The 3 most targeted endpoints on WordPress are: wp-login.php, xmlrpc.php, and wp-admin. CDNs like Cloudflare provide application layer protection, but requests reaching your server still consume PHP-FPM and MySQL resources. fail2ban operates at the transport layer—packets from banned IPs are dropped at the kernel level.
Example: doktorbul.com has 79,000 doctor profiles, each a dynamic PHP page. During brute force attacks, the PHP-FPM pool was filling up and real users were getting 504 Gateway Timeout errors. After installing fail2ban, attack traffic was cut off before reaching the server, and legitimate users weren't affected. We didn't write any extra rules in Cloudflare for this, just edited 3 files on the server side.
fail2ban advantages:
- Open source, free, ready in Debian/Ubuntu repositories
- Works with any application since it parses log files (Nginx, Apache, SSH, Postfix)
- Ban duration, retry count, whitelist fully customizable
- Can use nftables or firewalld backend instead of iptables
Creating a Custom Jail File for WordPress
In fail2ban, a "jail" is a protection rule group. Each jail contains a filter (regex pattern), an action (ban command), and parameters. Creating a custom jail for WordPress means opening a new .conf file in the /etc/fail2ban/jail.d/ folder. I typically use the name wordpress.conf.
First step: fail2ban installation (Ubuntu/Debian):
sudo apt update
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
After installation, jail.conf and the filter.d/ directory are created in /etc/fail2ban/. Never edit jail.conf directly, override it in jail.local or under jail.d/. I prefer jail.d/wordpress.conf because it's modular and easy to track in version control.
Example jail file (/etc/fail2ban/jail.d/wordpress.conf):
[wordpress-auth]
enabled = true
filter = wordpress-auth
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 600
bantime = 3600
port = http,https
action = iptables-multiport[name=wordpress, port="http,https", protocol=tcp]
Parameter explanation:
- enabled: is the jail active (true/false)
- filter: which regex pattern to use (file name under filter.d/)
- logpath: which log file to monitor (Nginx or Apache access.log)
- maxretry: how many failed attempts before ban (I recommend 5)
- findtime: time window for retry counting, in seconds (600 = 10 minutes)
- bantime: how long the IP will be blocked, in seconds (3600 = 1 hour)
- port: which ports will be affected (http=80, https=443)
- action: ban command (iptables-multiport is standard)
With these settings, an IP that tries the wrong password 5 times on wp-login.php within 10 minutes gets banned from all HTTP/HTTPS traffic for 1 hour. On kamupersonelhaber.com, we use findtime=300 (5 minutes) because we publish 50+ job postings daily and bot traffic is aggressive.
Separate Jail for xmlrpc.php
xmlrpc.php is WordPress's XML-RPC API endpoint, used by legacy mobile apps and Jetpack. However, it's the most exploited file in DDoS attacks. If you're not using xmlrpc, disable it completely (.htaccess or Nginx config). If you are using it, create a separate jail:
[wordpress-xmlrpc]
enabled = true
filter = wordpress-xmlrpc
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 300
bantime = 7200
port = http,https
action = iptables-multiport[name=wordpress-xmlrpc, port="http,https", protocol=tcp]
For xmlrpc, maxretry=3 and bantime=7200 (2 hours) are stricter because legitimate usage is rare. On diolivo.com.tr, we completely disabled xmlrpc—it wasn't needed for CartBounty cart recovery.
Writing Filter Regex: wordpress-auth.conf
The jail file defines which IPs to ban, but the filter determines the "failed login" pattern. Filter files are under /etc/fail2ban/filter.d/ and use Python regex. For WordPress, there are two types of log patterns: wp-login.php POST request and HTTP 200 response (failed login also returns 200, cookie check required).
Example filter file (/etc/fail2ban/filter.d/wordpress-auth.conf):
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php HTTP.*" 200
^<HOST> .* "POST /wp-admin/admin-ajax\.php HTTP.*" 200.*wp_login_failed
ignoreregex =
Explanation:
- failregex: matching lines count as "failed attempts"
: fail2ban's special placeholder, automatically captures IP address - ^
: IP must be at the start of the line (Nginx access.log format) - POST /wp-login\.php: POST request to login endpoint
- HTTP.*" 200: HTTP response code 200 (failed login also returns 200)
- ignoreregex: to exclude specific IPs or user-agents (can leave empty)
This regex is optimized for Nginx access.log. If you're using Apache, the log format is different and the
203.0.113.45 - - [15/Jan/2025:14:23:11 +0000] "POST /wp-login.php HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
This line matches the failregex, and IP 203.0.113.45 is added to the retry counter.
xmlrpc Filter
Separate filter for xmlrpc.php (/etc/fail2ban/filter.d/wordpress-xmlrpc.conf):
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php HTTP.*" 200
^<HOST> .* "POST /xmlrpc\.php HTTP.*" 403
ignoreregex =
xmlrpc attacks typically come via the system.multicall method, executing 100+ commands in a single POST request. Both HTTP 200 and 403 (forbidden) are suspicious. On italyanmutfagi.com, xmlrpc is completely disabled, so we didn't need this filter.
Testing Process: fail2ban-regex and Live Attack Simulation
After writing the filter, test it before going to production. The fail2ban-regex command takes the log file and filter as parameters and shows how many lines matched.
Test command:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf
Example output:
Running tests
=============
Use failregex filter file : wordpress-auth, basedir: /etc/fail2ban
Use log file : /var/log/nginx/access.log
Use encoding : UTF-8
Results
=======
Failregex: 47 total
|- #) [# of hits] regular expression
| 1) [47] ^<HOST> .* "POST /wp-login\.php HTTP.*" 200
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [1234] Day/MON/Year:Hour:Minute:Second
`-
Lines: 1234 lines, 0 ignored, 47 matched, 1187 missed
47 matched means the filter is working. If you see 0 matched, the regex is incorrect—check the log format. I typically see 10-20 matched lines; on kamupersonelhaber.com, we see 200-300 matched daily because bot traffic is heavy.
For live testing, make fake login attempts with curl:
for i in {1..6}; do
curl -X POST https://example.com/wp-login.php \
-d "log=admin&pwd=wrongpassword" \
-H "User-Agent: TestBot"
sleep 2
done
Make 6 attempts (since maxretry=5, you should get banned on the 6th attempt). Then check fail2ban status:
sudo fail2ban-client status wordpress-auth
Output:
Status for the jail: wordpress-auth
|- Filter
| |- Currently failed: 1
| |- Total failed: 6
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 203.0.113.45
IP is banned. Check in iptables:
sudo iptables -L -n | grep 203.0.113.45
Output:
DROP all -- 203.0.113.45 0.0.0.0/0
Ban is working. Now try curl from that IP—you'll get a connection timeout.
Real Case: 87% CPU Reduction with fail2ban on kamupersonelhaber.com
On kamupersonelhaber.com, we publish 50+ public job postings daily, pulled from the ilan.gov.tr API. In November 2024, a coordinated brute force attack started from Chinese and Russian IPs, 200 requests per minute to wp-login.php. Cloudflare rate limit was set to 100 req/min but it wasn't sufficient—some IPs were using rotating proxies.
Server: 4 core CPU, 8 GB RAM, Nginx + PHP-FPM 8.2. CPU usage hit 92%, PHP-FPM pool was full, real users were getting 504 errors. We installed fail2ban in 15 minutes:
1. wordpress.conf jail file: maxretry=3, findtime=300, bantime=7200 2. wordpress-auth.conf filter: wp-login.php and admin-ajax.php POST requests 3. Test: 120 matched lines with fail2ban-regex 4. Restart: sudo systemctl restart fail2ban
Within 30 minutes, 47 IPs were banned. CPU dropped to 18%, PHP-FPM pool usage fell to 30%. Request count in Cloudflare Analytics didn't change (they're still visible at the CDN layer), but requests reaching the server decreased by 78%. We didn't use any paid services for this, just open-source tools.
Additional optimization: We synchronized the fail2ban banned IP list with Cloudflare WAF. We added a Cloudflare API call to the fail2ban action script—banned IPs are automatically added to Cloudflare firewall rules. This way they're also blocked at the CDN layer and never reach the server.
Whitelist and False Positive Management
fail2ban with aggressive settings can ban your own IP. Especially when testing in staging environments or if your office IP is static, add a whitelist. Create the /etc/fail2ban/jail.local file:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.100 198.51.100.0/24
ignoreip parameters:
- 127.0.0.1/8: localhost
- ::1: IPv6 localhost
- 203.0.113.100: your office IP
- 198.51.100.0/24: entire subnet (CIDR notation)
On diolivo.com.tr, we added the e-commerce manager's IP to the whitelist because they logged into the admin panel 50+ times daily and sometimes made password mistakes.
For false positives, manually unban the IP:
sudo fail2ban-client unban 203.0.113.45
Or clear all banned IPs:
sudo fail2ban-client unban --all
Check ban logs:
sudo tail -f /var/log/fail2ban.log
Output:
2025-01-15 14:23:45,123 fail2ban.actions [12345]: NOTICE [wordpress-auth] Ban 203.0.113.45
2025-01-15 15:23:45,456 fail2ban.actions [12345]: NOTICE [wordpress-auth] Unban 203.0.113.45
Ban and unban operations are logged. I typically do weekly log analysis to check which IPs are frequently banned. If the same IP keeps getting banned, you can add a permanent ban (manual iptables rule).
Advanced: Cloudflare API Integration and Notifications
fail2ban's action mechanism is customizable. The default action adds rules to iptables, but you can add Cloudflare API, Slack webhooks, or email notifications. I prefer Cloudflare integration because it also blocks at the CDN layer.
Cloudflare action file (/etc/fail2ban/action.d/cloudflare.conf):
[Definition]
actionban = curl -X POST "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/firewall/access_rules/rules" \
-H "X-Auth-Email: <EMAIL>" \
-H "X-Auth-Key: <API_KEY>" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"}}'
actionunban = curl -X DELETE "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/firewall/access_rules/rules/<RULE_ID>" \
-H "X-Auth-Email: <EMAIL>" \
-H "X-Auth-Key: <API_KEY>"
Replace the
action = iptables-multiport[name=wordpress, port="http,https", protocol=tcp]
cloudflare[email="info@futia.net", api_key="xxxxx", zone_id="yyyyy"]
This way, bans are added to both iptables and Cloudflare. We use this method on doktorbul.com—dual protection at server + CDN layer.
For Slack notifications, webhook action:
[Definition]
actionban = curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H "Content-Type: application/json" \
--data '{"text":"fail2ban: <ip> banned from <name>"}'
I prefer email over Slack—less noise. fail2ban has a built-in sendmail action, add it to the jail file:
action = iptables-multiport[name=wordpress, port="http,https", protocol=tcp]
sendmail-whois[name=wordpress, dest=info@futia.net, sender=fail2ban@futia.net]
You'll receive an email for each ban, including the IP's whois information.
Performance and Resource Usage
fail2ban is written in Python and continuously monitors log files (uses inotify). Resource consumption is minimal: 20-30 MB RAM, 1-2% CPU. However, with very large log files (10 GB+), parse time can increase. I recommend two optimizations:
1. Log rotation: rotate daily with logrotate, compress old logs 2. Selective logging: in Nginx, only log 4xx and 5xx responses (access_log directive)
Example logrotate config (/etc/logrotate.d/nginx):
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
This config rotates daily, keeps for 14 days, and compresses. fail2ban automatically detects the new log file.
On italyanmutfagi.com with 618 recipes, each a separate page, the log file grows 2 GB daily. Without logrotate, fail2ban parse time was reaching 5 seconds; after logrotate, it dropped to 0.5 seconds.
Alternative Protection Methods and fail2ban Comparison
There are other methods for WordPress brute force protection:
1. Cloudflare Rate Limiting: CDN layer protection, 10,000 requests/month free, then paid 2. Wordfence Plugin: WordPress plugin, PHP-level protection, uses database 3. Limit Login Attempts Plugin: simple plugin, adds delay instead of IP ban 4. .htaccess IP whitelist: wp-admin access only for specific IPs 5. Nginx rate limiting: limit_req_zone directive, application layer protection
fail2ban advantages:
- Free, open source
- Kernel-level protection (iptables), doesn't burden PHP-FPM
- Works for SSH, Postfix, FTP beyond WordPress
- Effective against attacks that bypass Cloudflare and hit the server directly
fail2ban disadvantages:
- Requires server access (can't use on shared hosting)
- Requires regex writing knowledge
- Ineffective against attacks using rotating proxies (each request from different IP)
I typically recommend fail2ban + Cloudflare combination. Cloudflare at CDN layer, fail2ban at server layer. On memuratamalari.com, we use both together—95%+ of bot traffic is blocked.
Plugins like Wordfence write every failed login to the database, increasing MySQL load. fail2ban reads the log file, doesn't touch the database. On diolivo.com.tr, we removed Wordfence and switched to fail2ban—MySQL query count decreased by 22%.
If brute force attacks on your WordPress site are increasing CPU usage and filling the PHP-FPM pool, fail2ban installation takes 10 minutes. I've shared the jail file, filter regex, and testing process in this post. You can implement it on your own server and monitor ban logs. If you need technical support, you can reach out via WhatsApp: +90 532 491 17 05. As FUTIA, we provide automation + security + monthly maintenance services for WordPress sites—fail2ban installation is included in our standard package.
Frequently Asked Questions
Can fail2ban block xmlrpc.php attacks on WordPress?
Yes, you can create a separate jail and filter for xmlrpc.php. xmlrpc attacks typically come via the system.multicall method, executing 100+ commands in a single POST request. Capture the POST /xmlrpc.php pattern in the filter regex, and use strict parameters like maxretry=3 and bantime=7200 (2 hours). If you're not using xmlrpc, it's safer to disable it completely via .htaccess or Nginx config.
How do you optimize maxretry and findtime in fail2ban jail files?
maxretry is the number of failed attempts, findtime is the time window (in seconds). For aggressive protection, use maxretry=3 and findtime=300 (5 minutes)—meaning ban after 3 wrong attempts in 5 minutes. On e-commerce sites where customers may forget passwords, maxretry=5 and findtime=600 (10 minutes) is more balanced. bantime is the ban duration—3600 (1 hour) is standard, but for repeat attacks you can increase to 7200 (2 hours) or 86400 (1 day).
I'm getting 0 matched during fail2ban-regex testing, what should I do?
This means the regex pattern doesn't match the log format. First, manually check the log file: sudo tail -n 50 /var/log/nginx/access.log. Make sure the IP address is at the start of the line and the POST /wp-login.php expression appears in the log. Nginx and Apache log formats differ—the <HOST> placeholder position may vary. Run fail2ban-regex in verbose mode: fail2ban-regex -v /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf. It shows which lines matched and which were missed.
How do you integrate fail2ban with Cloudflare?
You can automatically add banned IPs to firewall rules using the Cloudflare API. Create the /etc/fail2ban/action.d/cloudflare.conf file and send a POST request to the Cloudflare API via curl in the actionban directive. Pass your Zone ID, API Key, and email as parameters. Add the cloudflare action to the action line in the jail file. This way, IPs are banned in both iptables and Cloudflare, blocked at the CDN layer too. I shared a detailed config example in the post.
Can I use fail2ban on shared hosting?
No, fail2ban requires root access because it modifies iptables rules. On shared hosting, you're not the server administrator and can't install fail2ban. As an alternative, you can use WordPress plugins like Wordfence or Limit Login Attempts. However, these plugins operate at the PHP level and consume server resources. If you have a serious brute force problem, I recommend switching to VPS or dedicated server—fail2ban installation takes 10 minutes.
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.