Lock Down wp-login.php, xmlrpc.php and wp-admin
By Stu 7 min read
Three URLs on every WordPress site take the overwhelming majority of hostile traffic: wp-login.php, xmlrpc.php, and everything under /wp-admin/. If you tail the access log of a WordPress site that's been online for more than a week, those three paths will dominate. Bots don't guess. They know exactly where to knock.
This post is about how to lock each one down properly. Not a plugin recommendation. The actual server configuration, with the trade-offs of each approach, and why the wrong lockdown can be worse than none.
Why these three, and not others
WordPress URLs are predictable. Every WordPress site in the world has the same login page at the same path, the same XML-RPC endpoint at the same path, and the same admin area at the same path. That predictability is what makes them targets. An attacker's scanner doesn't need to know what CMS you're running. It just fires the same request at millions of domains and sees what answers.
The good news is that the same predictability that makes them targets also makes them easy to lock down. You know exactly which paths to protect, and everything else on the site can carry on serving traffic normally.
wp-login.php
The single most-attacked file on any WordPress site. Brute force. Credential stuffing. XML-RPC-amplified brute force. If your site is on the internet, wp-login.php is being hit right now.
There are three sensible approaches, from softest to hardest.
Rate limiting is the baseline. Any decent security plugin (Wordfence, Solid Security, Limit Login Attempts Reloaded) will block an IP after a handful of failed attempts. Fail2Ban does the same job at the server level by watching the nginx access log for 200 responses to wp-login.php POSTs followed by failed authentications. Either works. Doing both is fine and gives you defence in depth.
Rate limiting doesn't stop attackers who use a botnet with thousands of IPs and only try a few passwords per IP. It stops the far more common case of a single IP hammering the endpoint. Which is 90% of what's happening in your log.
Moving the login URL off wp-login.php is the middle option. A plugin like WPS Hide Login changes the login path to something you choose. wp-login.php starts returning 404s to the whole world, and only you know the actual URL.
Security theatre? Sort of. A determined attacker who's targeting your specific site can still find the real URL. What it does is remove you from the automated scanning that's aimed at wp-login.php. And that automated scanning accounts for the vast majority of login attacks. Being off the well-lit road matters, even if it isn't a moat.
The catch is that if you're running any tooling that talks to wp-login.php directly (some backup services, some monitoring tools), you'll need to update it. Test first.
IP allow-listing is the hardest option. In your nginx config:
location = /wp-login.php {
allow 203.0.113.42; # your office IP
allow 198.51.100.0/24; # your VPN range
deny all;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
}
Now wp-login.php returns 403 to everyone except the specified IPs. This is genuinely bulletproof against remote brute force, because remote attackers can't reach the endpoint at all. It's also genuinely painful the first time you're locked out of your own site from a hotel wifi.
If you have a static IP or a VPN with a static exit, this is the right answer. If you don't, it isn't.
Cloudflare route is worth mentioning. If Cloudflare sits in front of your site, you can restrict wp-login.php at the Cloudflare rules level to specific countries, specific IPs, or specific Cloudflare Access authenticated users. That gets you IP allow-listing without needing a static IP yourself.
xmlrpc.php
XML-RPC is a legacy WordPress interface that used to matter and mostly doesn't any more. It exists so external clients (the WordPress mobile app, Jetpack, Windows Live Writer, various publishing tools) can talk to your site without going through the admin.
The problem is that xmlrpc.php has two features attackers love. Amplified brute force, where a single request can attempt hundreds of passwords in one go, sidestepping any rate limiting that counts by request. And pingback DDoS reflection, where your site can be used as an unwilling participant in attacks on someone else.
If you don't use XML-RPC, and most sites don't, block it entirely at the nginx level:
location = /xmlrpc.php {
deny all;
return 404;
}
That's it. The endpoint becomes a 404 for everyone, and every WordPress site sees an immediate drop in log noise.
How to know if you use it if you're not running Jetpack, not using the WordPress mobile app to publish, and not using an external editor that connects over XML-RPC, you don't use it. If in doubt, block it and watch for anything that breaks over the next week. Almost nothing will.
If you do use Jetpack, don't block XML-RPC entirely. You can still limit it, but you'll need to allow Jetpack's IPs through, which changes periodically. In practice, if you need Jetpack, leave XML-RPC alone and rely on rate limiting plus strong passwords.
wp-admin
The admin area is different from the other two. It's supposed to be reachable. That's the whole point of it. What you're protecting against isn't attackers hitting the URL, it's attackers reaching the URL with a valid session or leveraging a plugin vulnerability inside it.
The single most effective lockdown for /wp-admin/ is the same as wp-login.php: IP allow-listing at the nginx level. If you and your team log in from a small number of known IPs or a VPN, restrict /wp-admin/ to those addresses:
location /wp-admin/ {
allow 203.0.113.42;
allow 198.51.100.0/24;
deny all;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
}
}
The one file you have to leave open is admin-ajax.php. Front-end plugins use it to make AJAX requests, so if you block it, half the site breaks. Carve it out:
location = /wp-admin/admin-ajax.php {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
}
That location block wins because nginx matches exact = locations before prefix locations. admin-ajax.php stays public, everything else in /wp-admin/ is locked down.
Test this carefully. Get it wrong and legitimate visitors will see broken pages. Deploy to staging, hit every user-facing page, watch the browser console for AJAX failures.
The order to do these in
If you're doing this from scratch, the order that gives you the biggest wins fastest:
- Enable 2FA on every admin account. This is the single most valuable thing on the whole page and it takes ten minutes.
- Block
xmlrpc.phpat the nginx level, unless you know you need it. Immediate reduction in attack surface, zero cost. - Install rate limiting on
wp-login.php(any plugin, or Fail2Ban). Handles the vast majority of brute force. - Move the login URL off
wp-login.php. Removes you from automated scanning noise. - IP allow-list
/wp-admin/if you have a static IP or VPN. The strongest single lockdown available.
You don't need to do all five. Doing the first two takes half an hour and closes off most of what actually happens to WordPress sites.
The one lockdown that's actively bad
Blocking xmlrpc.php at a level that returns a 500 error instead of a clean 404 or 403. Some old guides suggest denying the file via PHP itself, which means every request still spawns a PHP process before being rejected.
That's worse than doing nothing. You've spent server resources on the request without stopping any of the abuse. Block it at the nginx level (or Apache, on older setups) so the request never touches PHP.
Where this fits
Login and admin hardening is Chapter 3 of the handbook (authentication) and Chapter 7 (firewall and intrusion prevention). The book covers each of these lockdowns in more detail, including the exact configuration for different hosting setups, and the failure modes for each one.
If this post left you wanting the reasoning at more depth, or a wider set of hardening measures, that's what's in the book.
Get the book for £19.
Get the free WordPress Security Checklist
The security checks I'd run through on any WordPress site, delivered straight to your inbox.
Want to go deeper?
The first chapter of Protect My WP is free. Start with the foreword, then read Chapter 1 on hosting and server security. There is also a shorter guide that walks the same ground faster if you want the shape of the book first.