Brute force bots hammer wp-login.php on every WordPress site, guessing passwords thousands of times a day from constantly changing IP addresses. If your site runs behind Cloudflare, you can stop almost all of that traffic at Cloudflare's edge with a single WAF rule, before it ever reaches your server. This guide shows you how to build that rule correctly, including the one detail most guides get wrong that quietly lets the attacks through.
Create one Cloudflare WAF custom rule that shows a Managed Challenge on login and admin requests. Automated bots cannot solve the challenge, so they are stopped at Cloudflare's edge and never reach your server. Real visitors pass through almost invisibly. The trick is to match the login path with contains, not equals, so attackers cannot slip past it by adding a trailing slash to the URL.
Cloudflare can only filter traffic that actually passes through it. This rule protects requests that reach your site through Cloudflare's network. It does not help with:
- Requests sent straight to your server's IP address, bypassing Cloudflare
- Attacks from inside your own WordPress (a compromised plugin or theme)
- Password guessing against services other than the website, such as FTP or email
That is why the last two sections cover hardening WordPress itself. The Cloudflare rule stops the flood at the edge; the WordPress side catches anything that gets through.
01. Why wp-login.php Gets Attacked
WordPress powers a huge share of the web, and every WordPress site has its login form at the same place: /wp-login.php. That predictability makes it a standing target. Automated bots crawl the internet looking for WordPress sites and then submit login attempt after login attempt, trying common usernames like "admin" against lists of leaked passwords.
You will usually notice it one of three ways:
- A flood of failed-login notification emails if you run a security plugin that reports them.
- The site feeling slow, because every login attempt loads WordPress, runs PHP, and queries the database, which burns through your account's resources.
- Resource-limit warnings or a temporary account suspension when the attack is heavy enough. See Error 508/503 Resource Limit Reached.
The attacks almost always come from many different IP addresses, often through Tor or hijacked machines, so blocking one address at a time never ends. The effective answer is to challenge the request pattern itself, which is exactly what a Cloudflare WAF rule does.
A login-limiting plugin helps, but it only acts after the request has already reached your server and started WordPress. The whole point of filtering at Cloudflare is to stop the request before it costs you any PHP or database work. Use both: Cloudflare to absorb the volume, a plugin as a backstop.
02. Before You Start: Proxy Your Site Through Cloudflare
Cloudflare only sees your traffic when your DNS records are proxied through it. In the Cloudflare dashboard, open DNS and check that the records for your site (usually the root domain and www) show an orange cloud, not a grey one. A grey cloud means Cloudflare is only answering DNS and your traffic goes straight to the server, where no WAF rule can touch it.
If you have not set Cloudflare up yet, start with our Cloudflare Setup Guide. The rule below works on every Cloudflare plan, including the free one.
Keep your real server IP address out of public view. If a DNS record anywhere (an old mail or ftp subdomain, for example) points directly at the server with a grey cloud, attackers can find that IP and skip Cloudflare entirely. Proxy what you can, and avoid publishing the raw IP.
03. Build the Login Protection Rule
In the Cloudflare dashboard, select your domain, then go to Security > WAF > Custom rules (in the newer dashboard layout this is under Security > Security rules). Click Create rule and give it a name like WordPress Login Protection.
Cloudflare lets you build the condition with dropdown menus or paste it as an expression. The expression is exact and easy to copy, so switch the rule to the Edit expression view and paste this in:
(http.request.uri.path contains "/wp-login.php" and http.request.method eq "POST")
or (http.request.uri.path contains "/wp-admin"
and not http.request.uri.path contains "/wp-admin/css/"
and not http.request.uri.path contains "/wp-admin/js/"
and not http.request.uri.path contains "/wp-admin/images/"
and http.request.uri.path ne "/wp-admin/admin-ajax.php")
Then set the action:
- Choose action: Managed Challenge
- Place at: leave the default order
Click Deploy. That is the whole rule. The next two sections explain the two parts that make it both effective and safe, because this is where most guides go wrong.
A Cloudflare zone covers one domain, so a rule in that zone only affects that domain and you do not normally need to name the host. If you point several domains at one zone using subdomains, scope the rule to the right site by adding http.host eq "yourdomain.com" and (...) around the condition above.
04. Match the Whole Login Path, Not Just the Exact File
This is the detail that matters most, and the one that quietly defeats a lot of login-protection rules.
Most guides tell you to match the path where it equals /wp-login.php. That looks correct, but it only matches that exact text. Attackers know this, so they send the request to /wp-login.php/ instead, with a trailing slash. Web servers still hand that off to the same login script and process the password attempt, but a rule that checks for an exact match sees /wp-login.php/ as a different path and lets it straight through. The attack continues as if you had no rule at all.
The fix is to match with contains rather than equals:
Weak (misses the trailing-slash trick):
http.request.uri.path eq "/wp-login.php"
Strong (catches /wp-login.php and /wp-login.php/ and anything after):
http.request.uri.path contains "/wp-login.php"
The rule in the previous section already uses contains, so if you pasted it you are covered. If you built your rule by hand with the dropdowns, open it and confirm the operator on the login path reads contains, not equals.
The same idea applies to any path-based rule you write. A single trailing character, an extra slash, a mixed-case spelling, or a query string can all be used to dodge a too-exact match. When you are blocking a specific script, matching on contains the script name is far harder to evade than an exact match.
05. Keep the Login Page and Dashboard Working
Two choices in the rule keep it from getting in the way of real use.
Only challenge the POST, not the page view
Loading the login page is a GET request. Submitting the login form is a POST. The rule challenges only the POST, so your login page still loads normally for everyone, and the challenge appears only at the moment credentials are actually submitted. That keeps the experience clean for real people while still stopping the bots, which have to POST to guess a password.
Let the dashboard load its own files
The /wp-admin part of the rule protects the admin area, but the dashboard also loads its stylesheets, scripts, and images from under /wp-admin/. If you challenge those, the login and admin pages render unstyled and broken. The rule excludes /wp-admin/css/, /wp-admin/js/, and /wp-admin/images/ so those load freely. It also excludes admin-ajax.php, which many themes and plugins call from the public front end for things like contact forms, so leaving it challenged would break features for ordinary visitors.
Managed Challenge is the right default. It is nearly invisible to real browsers, automatically harder on suspicious ones, and it does not permanently lock anyone out. Use the stronger Block action only if you are certain no legitimate person needs that path, because a block with a mistake in it can lock you out of your own admin area.
Cloudflare's paid plans add Rate Limiting rules, which can challenge or block an IP only after it makes, say, more than five POSTs to /wp-login.php in a minute. That is an excellent complement to the rule above, because it reacts to behavior rather than just the path. The custom rule in this guide works on every plan and is the right starting point.
06. Shut Down XML-RPC Too
WordPress has a second door that attackers love: /xmlrpc.php. It is an older remote-access interface that lets a single request try many passwords at once (through a feature called system.multicall) and can also be abused to bounce junk traffic off your site. Most modern sites do not need it.
If you are sure nothing you use depends on it, add a second Cloudflare custom rule to block it outright:
Field: URI Path
Operator: contains
Value: /xmlrpc.php
Action: Block
A few tools still use XML-RPC, most notably the Jetpack plugin and the official WordPress mobile app. If you rely on one of those, use Managed Challenge instead of Block, or skip this rule and disable the specific XML-RPC features you do not need from inside WordPress. Our guide on disabling comments, pingbacks, and trackbacks covers the pingback piece that XML-RPC is most often abused for.
07. Harden WordPress Itself
The Cloudflare rule absorbs the volume. These steps make sure anything that does reach WordPress still fails. None of them take long and together they close the gap:
- Do not use "admin" as a username. If your administrator account is named admin, create a new administrator with a different name, log in as that account, and delete the old one, assigning its content to the new account.
- Use a long, unique password for every WordPress account, especially administrators. A password manager makes this painless. A strong password alone defeats the entire point of password guessing.
- Add two-factor authentication with a reputable plugin. Even a correctly guessed password is useless without the second factor.
- Install a login-limiting plugin as a server-side backstop, so repeated failures from a single source are throttled even if they reach WordPress.
- Keep WordPress, themes, and plugins updated, and remove anything you no longer use. Most hacked sites are compromised through outdated code, not guessed passwords.
For the full checklist, see WordPress Security: The Complete Hardening Guide. If you suspect a site has already been compromised, work through How to Fix a Hacked WordPress Site.
08. Test the Rule Without Locking Yourself Out
After you deploy the rule, confirm it behaves the way you expect:
- Load your login page at
yourdomain.com/wp-login.phpin a normal browser. It should open normally, because viewing the page is a GET and only the POST is challenged. - Log in as usual. With Managed Challenge, a real browser passes through automatically or after a brief check. If you are asked to confirm you are human, that is expected and only takes a moment.
- Watch it work. In the Cloudflare dashboard under Security > Events, you will see challenged login attempts listed against your new rule. During an active attack this fills up quickly, which confirms the rule is catching them.
If you choose the Block action anywhere instead of Managed Challenge, create a higher-priority Skip rule for your own IP address first, so you are never blocked from your own admin area. If a server-level firewall ever blocks you after too many failed attempts, follow Firewall Block: How to Unblock Your IP to get back in.
Want Us to Set This Up for You?
If you would rather we review your Cloudflare configuration or build these rules for you, open a ticket and we will take care of it.
Open a Support TicketQuick Recap: Stopping WordPress Login Attacks
If you only do these things, do these:
- Proxy your site through Cloudflare so the orange cloud is on and your traffic actually passes through the WAF
- Create a Managed Challenge custom rule on the POST to your login path and on the admin area
- Match the login path with contains, not equals, so the trailing-slash trick (
/wp-login.php/) cannot slip past it - Exclude the admin css, js, images, and admin-ajax.php so the dashboard and front-end features keep working
- Harden WordPress too with a non-admin username, a strong password, and two-factor login as a backstop
