Every public WordPress site gets brute-force login attempts: bots trying common usernames and leaked passwords against wp-login.php and xmlrpc.php, thousands of times a day. Besides the risk of a break-in, the attempts waste PHP workers and CPU. Limiting them works best in layers, and on a VPS you can stop most attempts before they reach PHP.
Layer 1: a login-limiting plugin
Plugins count failed logins per IP, and lock out offenders for a while:
- Limit Login Attempts Reloaded: simple and popular.
- Wordfence and Solid Security: include login limits along with firewall and scanning features.
- WP Cerber and others offer similar features.
Typical settings: 4–5 retries, 20-minute lockout, longer lockouts after repeated lockouts.
Limitations: every attempt still loads WordPress and PHP, so it doesn’t save server resources during a big attack.
Layer 2: Nginx rate limiting (cheap and effective)
Rate limits work before PHP runs. In nginx.conf (http block):
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;
In the site’s server block:
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
limit_req_status 429;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Real users rarely hit 10 login requests per minute. Bots get 429 Too Many Requests. See the ideal Nginx config for WordPress and Nginx rate limiting.
Layer 3: Fail2ban (ban at the firewall)
Fail2ban watches the access log, and bans IPs that repeatedly POST to the login page:
# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 6
findtime = 10m
bantime = 1h
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-login
This counts all POSTs (successful logins too), so set maxretry generously. For “failed logins only”, plugins such as WP fail2ban log failures to syslog or auth.log, with a matching filter. See Fail2ban install and configure.
Behind Cloudflare, make sure logs contain the real visitor IP, or you’ll ban Cloudflare. See real IP behind Cloudflare.
Layer 4: block XML-RPC
xmlrpc.php lets attackers try many passwords in a single request (the system.multicall method), which bypasses per-request limits. If you don’t use the WordPress mobile app or Jetpack features that need it, block it:
location = /xmlrpc.php { deny all; access_log off; }
See disable XML-RPC.
Layer 5: make attempts pointless
- Strong, unique passwords for every account with publishing rights.
- Two-factor authentication for administrators and editors. See WordPress two-factor authentication.
- Don’t use
adminas a username, and hide author usernames where possible. - Optionally restrict
wp-adminandwp-login.phpto your IPs or a VPN if only a few people log in:
location = /wp-login.php {
allow 203.0.113.50;
deny all;
# … fastcgi settings
}
Avoid locking yourself out
- Whitelist your own IP in the plugin and in Fail2ban (
ignoreip = 127.0.0.1/8 203.0.113.50). - If you get locked out: wait for the lockout to expire, unban with
sudo fail2ban-client set wordpress-login unbanip 203.0.113.50, or deactivate the plugin with WP-CLI. See reset the WordPress admin password.
Check that it works
grep -c 'POST /wp-login.php' /var/log/nginx/access.log
grep ' 429 ' /var/log/nginx/access.log | tail
sudo fail2ban-client status wordpress-login
Quick reference
| Layer | Tool | Stops before PHP? |
|---|---|---|
| Lockouts per IP | Login-limiting plugin | No |
| Rate limit | Nginx limit_req |
Yes |
| Firewall bans | Fail2ban | Yes |
| Multicall attacks | Block xmlrpc.php |
Yes |
| Stolen passwords | 2FA | — |
Related: WordPress hardening and WordPress VPS hosting.