If you've ever checked your WordPress server logs, you've seen it: a steady stream of requests to
/wp-login.php and /wp-admin from IPs you've never heard of, at all hours, with no referrer.
That's not targeted. It's automated. Every WordPress site on the internet gets this traffic,
because the login URL is the same on every default install, which means a bot written once works
against millions of sites forever.
Why the default login path is a structural problem, not a bug
WordPress can't ship a random login URL by default. Plugins link straight to wp-login.php,
documentation assumes it exists, and a huge number of integrations (password reset emails, SSO
redirects, staging-to-production migrations) depend on that path being predictable. Changing it
is an opt-in hardening step you take on your own site, not something core can fix for everyone at
once without breaking things that currently work.
That's why "just rename wp-login.php" isn't a core patch waiting to happen — it's a trade-off
between predictability (which every integration relies on) and obscurity (which only helps once
you've opted in).
What actually happens during a credential-stuffing run
Watch the pattern for a few minutes on an unprotected site and it's remarkably consistent:
- Request rate is steady, not bursty — slow enough to stay under most rate limits you'd set by accident, fast enough to try thousands of combinations a day.
-
Usernames cluster around
admin, the site's own domain name, and a handful of common first names — not random strings. These are credential lists built from breach dumps and common WordPress install conventions, not brute-force in the "try every character" sense. - No referrer, no JS execution — these are raw POST requests, not a browser driving a form.
This is also why rate-limiting beats CAPTCHA for this specific threat. A CAPTCHA assumes the
attacker is a human or a bot without a human fallback; credential-stuffing bots are neither —
they just don't have infinite patience for a 429 Too Many Requests response slowing them to a
crawl. Rate-limiting attacks the bot's actual constraint (time), where CAPTCHA attacks a
constraint it may not even have.
A minimal hook to see this on your own site
Before reaching for a plugin, you can see this pattern yourself with one hook:
<?php
add_action( 'wp_login_failed', 'prefix_log_failed_login' );
function prefix_log_failed_login( $username ) {
$ip_address = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
error_log( sprintf( 'Failed login for "%s" from %s', $username, $ip_address ) );
}
Tail your error log for a day on a site that's been live for more than a few weeks, and the
pattern above will be staring back at you — not as a theory, but as your own traffic.
Where a custom login URL and rate-limiting actually fit
Moving the login endpoint doesn't stop a targeted attacker who already knows your real URL —
anyone who's phished a password or found it in a leaked database isn't slowed down by this.
What it does is silence the roughly 99% of traffic that's just scanning the default path on every
WordPress site it can find, with no idea yours exists at a different one. Once that noise is
gone, the signal that's left in your logs — the handful of requests actually aimed at your
specific site — becomes much easier to see and act on. Pair that with rate-limiting on whatever
login path you do use, and you've addressed both the volume problem and the determined-attacker
problem with two independent, complementary controls.
This is exactly the free feature set in Loyal Security custom login URL plus brute-force protection, no paywall, no upsell screen in the
way. If you'd rather build and maintain the above yourself, the hook above is a genuine starting
point; if not, that's what the plugin is for.
We also cover this topic in plain language, for a non-technical audience, on the company blog:
The Two URLs Every Bot Tries First on Your WordPress Site.
Top comments (0)