Every WordPress site gets a steady stream of password-guessing requests against wp-login.php and xmlrpc.php. Even when they fail, each one runs PHP and queries the database, which wastes CPU and can slow the site down. Fail2Ban bans the attacking IPs at the firewall, so their requests never reach WordPress again.
The best approach: log only failed logins
Nginx’s access log can’t tell a failed login from a successful one, since both are POST /wp-login.php with a 200 or 302. The clean solution is to have WordPress log authentication failures to syslog, and let Fail2Ban watch those.
1. Install the WP fail2ban plugin
In WordPress, install WP fail2ban (by Charles Lecklider), or with WP-CLI:
wp plugin install wp-fail2ban --activate
It writes lines like these to the system auth log:
wordpress(example.com)[1234]: Authentication failure for admin from 198.51.100.7
wordpress(example.com)[1234]: XML-RPC authentication failure from 198.51.100.7
wordpress(example.com)[1234]: Blocked user enumeration attempt from 198.51.100.7
See WP-CLI.
2. Install the matching filters
The plugin ships Fail2Ban filters. Copy them into place (the path depends on the plugin version):
sudo cp /var/www/example.com/wp-content/plugins/wp-fail2ban/filters.d/wordpress-hard.conf /etc/fail2ban/filter.d/
sudo cp /var/www/example.com/wp-content/plugins/wp-fail2ban/filters.d/wordpress-soft.conf /etc/fail2ban/filter.d/
- wordpress-hard — things that should never happen legitimately (blocked user enumeration, attempts on non-existent users). Ban after 1 attempt.
- wordpress-soft — normal failed logins (wrong password). Allow a few.
3. Create the jails
# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-hard]
enabled = true
port = http,https
filter = wordpress-hard
logpath = /var/log/auth.log
backend = auto
maxretry = 1
bantime = 24h
[wordpress-soft]
enabled = true
port = http,https
filter = wordpress-soft
logpath = /var/log/auth.log
backend = auto
maxretry = 4
findtime = 10m
bantime = 2h
On systems without /var/log/auth.log (journald only, such as Debian 12+ without rsyslog), use backend = systemd and remove logpath. RHEL uses /var/log/secure.
sudo fail2ban-client reload
sudo fail2ban-client status wordpress-soft
Alternative: access-log filter (no plugin)
If you can’t add a plugin, count POSTs to the login endpoints in the Nginx log:
# /etc/fail2ban/filter.d/wp-login-post.conf
[Definition]
failregex = ^<HOST> -.*"POST /(wp-login\.php|xmlrpc\.php)
ignoreregex =
[wp-login-post]
enabled = true
port = http,https
filter = wp-login-post
logpath = /var/log/nginx/access.log
maxretry = 8
findtime = 5m
bantime = 1h
This counts all attempts, successful ones included, so set maxretry high enough for real users. Sites with many users logging in from one office IP should whitelist it. More Nginx jails are in Fail2Ban for Nginx.
Test the filters
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/wordpress-soft.conf
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wp-login-post.conf
Then try a few wrong passwords from a test IP (a phone on mobile data) and watch the ban appear:
sudo fail2ban-client status wordpress-soft
sudo fail2ban-client set wordpress-soft unbanip 198.51.100.7 # undo
Behind Cloudflare or a proxy
If traffic comes through Cloudflare, WordPress and Nginx see Cloudflare IPs unless real-IP handling is configured, and a firewall ban on the origin doesn’t stop requests arriving through Cloudflare. Configure real IP headers, and either use Fail2Ban’s Cloudflare action (which bans via the Cloudflare API) or block at Cloudflare with WAF rules. See real visitor IPs behind Cloudflare.
Reduce the attack surface too
Fail2Ban is one layer. Combine it with:
- Disable XML-RPC if you don’t need it (no Jetpack, mobile app or remote publishing). See disable XML-RPC.
- Rate-limit
wp-login.phpin Nginx:
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
See Nginx rate limiting.
- Strong passwords and 2FA for every admin account.
- Don’t use
adminas a username, and block user enumeration (/?author=1). - Keep WordPress, plugins and themes updated. See WordPress hardening.
Quick reference
| Task | Command |
|---|---|
| Install plugin | wp plugin install wp-fail2ban --activate |
| Filters | wordpress-hard.conf, wordpress-soft.conf |
| Reload | fail2ban-client reload |
| Status | fail2ban-client status wordpress-soft |
| Test filter | fail2ban-regex LOG FILTER |
| Unban | fail2ban-client set JAIL unbanip IP |
Related: Fail2Ban setup and WordPress on a VPS.