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 admin as a username, and hide author usernames where possible.
  • Optionally restrict wp-admin and wp-login.php to 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.

  • WordPress
  • Security
  • Fail2ban