Back

WordPress Brute Force: Close Both Doors

October 9, 2026 · Tech

Have you ever opened your inbox to a steady drip of "failed login attempt" emails from your WordPress site, while the failure counter in your login-protection plugin climbs and never comes down? That's bot brute force: machines running username and password dictionaries against your admin login, over and over.

Against this kind of attack, two changes are enough: move the login page off its default URL, and seal the backup entrance, xmlrpc.php, at the gateway. This post walks through the diagnosis, the steps, the verification checklist, and the traps that bite along the way.

TL;DR:

  • Read the logs first to classify the attack: rotating IPs from one /24 subnet, one or two tries per IP, generic dictionary usernames like admin. That's internet-wide spray, not a targeted campaign
  • Spray bots almost only ever hit the default /wp-login.php; renaming the login URL is the highest-leverage single move
  • Block xmlrpc.php with a 403 at the gateway so requests never reach PHP; after a config change, restart the container, because reload silently reads stale bytes

Read the attack before you fight it

Before touching anything, read the logs. The login-protection plugin's panel (Limit Login Attempts Security, which its page says serves 2 million WordPress sites) lays out the clues: failure counts, source IPs, attempted usernames.

Three features together settle what kind of attack this is. The IPs rotate within a single /24 subnet, each trying once or twice. The usernames are pure dictionary: admin, Admin, admin@wordpress.com. There's no probing of the site's content anywhere. Nobody targeted you; it's a botnet proxy pool spraying the whole internet, and your site is one line on the list.

That conclusion drives every later trade-off. A targeted attack calls for targeted defense; a spray attack only needs the script to stop being worth it. Bounce off your door once, and the bot moves to the next site on the list. There is no motive to grind.

And one thing comes before any hardening: confirm whether the existing defense has actually been breached. Lockout counters climbing steadily means every single shot was caught. The site was never compromised and the front end was never affected. Say that out loud to the team, so nobody makes sloppy moves out of "we've been hacked" panic.

Renaming the WordPress login URL is cut number one

WordPress puts no limit on failed login attempts by default1, which is what makes brute force so cheap. And spray bots almost only ever hit the default /wp-login.php. Move it, and you've relocated the shop door from the main street into an alley. Most passing bots simply never reach it. That's the first cut of the whole hardening, and the highest-leverage one.

Hiding the login page is half the job

For the rename, WPS Hide Login is a good pick, a very light plugin: it doesn't touch core files or add rewrite rules, it just intercepts requests2. Deactivate it and everything goes back to the way it was.

The first pitfall lives right here: if you activate the plugin and configure nothing, the login page lands on a weak default slug ("login"), which happens to sit in the bot dictionaries. After installing, you must set your own explicitly:

wp option update whl_page <your-new-slug>
wp rewrite flush

The second line isn't redundant: writing the option via WP-CLI doesn't trigger the plugin's own rewrite flush, so without a manual flush the new address may never take effect. Another "installed but not actually working" trap.

Three rules for picking the slug: people will hand-type it, so it must be memorable; related to the brand but not obvious, because a compound of brand words isn't in any dictionary; and it must dodge WordPress's reserved query vars (feed, author, and friends; the plugin rejects those).

Then close the loop with four checks:

RequestExpected
/wp-login.php404
/<your-new-slug>/200, login form renders
/wp-admin/ (not logged in)302 to a 404, faking a dead end
Debug logNo new PHP errors

Close the second door: xmlrpc.php

With the login page hidden, one path remains: xmlrpc.php. XML-RPC is WordPress's legacy remote interface covering posts, media, users, and more3. It accepts username-plus-password calls, which makes it a second login door the rename never touched. Once the front door moves, brute-force traffic drifts here on its own.

If the site uses no remote publishing and no pingbacks, the endpoint has no reason to exist. Reject it at the gateway layer (Caddy in this example), so requests never reach PHP at all:

handle /xmlrpc.php {
    respond 403
}

Three lines of config, two real-world pitfalls behind them.

The first is in validation. Before shipping the change, you'd run a config check in a container image matching production, and it can fail with "server block without any key is global configuration." Nothing is wrong with the config: the site address in it is an environment-variable placeholder, the validation container doesn't have the variable injected, and the empty expansion produces a bogus syntax complaint. Lesson: keep the validation environment's variables aligned with the runtime's, or the error you're shown has nothing to do with the actual disease.

The second is in activation. If the config file is bind-mounted into the container as a single file, version control swapping in a new file leaves the container holding the old file's inode. Reload reads the stale bytes and "succeeds" without applying anything. Under this mounting style, always restart rather than reload after a change. The cost is a few seconds of downtime.

When re-verifying, hit the endpoint with both GET and POST. POST is the method the bots actually call, and testing GET only means you locked the display door. Both return 403, front pages return 200, and this step is done. If remote publishing via a mobile app or Jetpack is ever needed, delete those three lines and restart, and the endpoint is back.

Going live: backup, verification matrix, and bookmarks

Before touching production, take a full database backup. In theory the change is reversible; "in theory" has no business appearing in a production runbook.

One deployment decision worth remembering: if the plugin files are already in place via version control, activating is enough on production, no need to install. Activation only writes to the database, unaffected by the "no file edits on this server" lock, and it skips making the production box download from wordpress.org, which isn't always reliable from a China-based server.

After going live, re-run the full verification matrix on production: old address 404, new address 200, logged-out /wp-admin/ redirected to a 404, xmlrpc 403 on both verbs, homepage and key pages 200, no cache headers on the login page. All green, zero front-end impact.

Then comes the step engineering most easily forgets: hand the new login address to everyone who logs into the back end, and make sure they bookmark it. The old address is now a 404; someone clicking an old bookmark sees a dead page. To anyone out of the loop, that is indistinguishable from "the site is down."

Results and what to take away

The after-action look:

CheckBeforeAfter
/wp-login.php200, login form renders404
New login addressDidn't exist200, logs in fine
/xmlrpc.php (POST)200, endpoint reachable403
Failed login recordsClimbing steadilyCurve drops away

For anyone maintaining WordPress sites, ordered by return on effort:

  1. Read the logs before acting: rotating subnets plus dictionary usernames means spray, and the cure for spray is making scripts unprofitable, not buying appliances
  2. Renaming the login URL is cut number one: mind the plugin's weak default "login"; set it explicitly and flush rewrites manually
  3. Whether you use xmlrpc.php decides whether you block it: no remote publishing or pingbacks means 403 at the gateway, and deleting three lines restores it
  4. Verify every item after changing: actually request each 404/200/403, both GET and POST
  5. Treat the new address as a deliverable: from the moment the old address 404s, the job isn't done until it's in the hands of everyone who logs in

This kind of hardening needs no new security product; the hours belong to reading logs, verifying, and handing over. The return on security hardening was never in what you buy. It's in what you understand: where the door is hidden, where the bots come from, and how each layer of defense proves it's alive. Get those three straight, and two changes are enough.

Was this article helpful?

Questions, corrections, or your own take. We read every piece of feedback.

Let's talk about your project

Most of the problems in this article — we've stepped in them and fixed them ourselves. Arshtech builds web systems, desktop software, and AI-assisted delivery for small businesses, working remotely at a per-project price.