← News
/PLAYBOOK · 2026-09-08

WordPress Login Hardening: Rate Limiting and Disabling XML-RPC

wp-login.php is the most brute-forced WordPress entry; xmlrpc.php is used for credential guessing and attack amplification. Nginx login-page rate limits, XML-RPC disable vs rate-limit, application-layer lockout, JetPack dependency checks, lockout recovery. Commands are copy-paste ready.

Applicable scenarios

Prerequisites

How it works

wp-login.php is WordPress's sole login entry. Attackers submit common username/password dictionaries at high frequency. Requests are legitimate POSTs; WordPress itself has no rate limit. xmlrpc.php's system.multicall packs hundreds of credential attempts into one request, amplifying brute-force efficiency 100x+. Pingback can be borrowed by third parties for amplification. Common fix: constrain at the web server — IP-based rate limit on the login page; return 403 or rate-limit xmlrpc.php.

Procedure

1. Define rate-limit zones

Add to the http block of the Nginx main config:

`limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=1r/m;`

rate=10r/m means at most 10 login requests per IP per minute. Enough for humans; scripts stall.

2. Apply rate limits in the site config and disable XML-RPC

`server {
    ...
    location = /wp-login.php {
        limit_req zone=wplogin burst=5 nodelay;
        limit_req_status 429;
        include snippets/fastcgi-php.conf;      # fill in with the site's original fastcgi config
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }

    location = /xmlrpc.php {
        # If no feature depends on XML-RPC, return 403 directly:
        return 403;
    }
}`

The login location must keep the original fastcgi config, or the login page will serve PHP source as a download. If the site still uses XML-RPC, replace return 403 with a limit_req rate-limit.

3. Login-failure lockout (application-layer supplement)

Nginx rate limits by request frequency and cannot detect slow brute force. Add lockout logic in the theme's functions.php:

`add_filter('authenticate', function($user, $username, $password){
    if (is_wp_error($user)) {
        $key = 'fl_' . md5($_SERVER['REMOTE_ADDR']);
        $fails = (int) get_transient($key);
        if ($fails >= 5) {
            return new WP_Error('locked', 'Too many attempts, please try again in 30 minutes');
        }
        set_transient($key, $fails + 1, 30 * MINUTE_IN_SECONDS);
    }
    return $user;
}, 30, 3);`

Locks the same IP for 30 minutes after 5 consecutive failures. Two-layer defense with Nginx rate limiting.

4. Reload Nginx

`nginx -t && nginx -s reload`

Verification

`# Rate-limit check: POST login requests in a short burst and watch the status codes
for i in $(seq 1 15); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://helixcyber.ai/wp-login.php
done
# First few should be 200 or 302; 429 after the rate is exceeded`

XML-RPC check: curl -s -o /dev/null -w "%{http_code}" https://helixcyber.ai/xmlrpc.php, should return 403. Then log in once from a browser to confirm a real user is not rate-limited.

FAQ

FAQ 1: What breaks if XML-RPC is disabled?

JetPack and its site stats/auto-publish, WordPress mobile app application-password login, and some third-party sync tools depend on XML-RPC. If you use those, keep the rate-limit approach instead of return 403, and disable pingback in settings. Pure content sites can usually disable it outright.

FAQ 2: After the change, wp-login.php downloads as a PHP file?

The new location block omitted fastcgi config; Nginx served PHP as a static file. Copy the include and fastcgi_pass lines from the existing location ~ \.php$ block into the new location, or nest the limit_req directives inside the original PHP location.

FAQ 3: Locked out by the rate limit?

Switch networks or verify with local curl on the server. Then temporarily raise rate or comment out the limit_req lines and reload. Shared office egress IPs aggregate and trip the limit; if needed, map admin IPs to a separate key or allowlist them, then restore the rules.

Summary

WordPress login hardening tightens the entry at the web server: limit_req caps wp-login.php to single-digit requests per minute; xmlrpc.php gets 403 when unused, rate-limited when needed; application-layer lockout covers slow brute force. After config, batch-curl to confirm 429 and 403, and verify fastcgi config is intact. Low cost; stops most automated brute-force traffic.