Why Your WordPress Login Page Shouldn’t Be Public

Go and look at your website’s logs. I’ll wait.

See all those requests for /wp-login.php and /xmlrpc.php from countries you’ve never done business with? That’s bots trying passwords, all day, every day. Every WordPress site on the internet gets them.

Here’s my question: why is your login page open to the entire planet in the first place?

Who actually needs to log in?

For most small business sites, the honest answer is: you, maybe a staff member, and your web developer. That’s a handful of people, usually from a handful of places.

Yet by default WordPress lets anyone on earth reach the login form and have a go. Strong passwords and two-factor login help, and you should have both, but they’re the last line of defence. Better to stop the attacker before they even see the form.

The fix: one Cloudflare rule

If your site is behind Cloudflare, you can block the login and admin pages for everyone except trusted IP addresses. It works on the Free plan.

Step 1: Make a list of trusted IPs. In the Cloudflare dashboard go to Manage account → Configurations → Lists and create an IP list (I call mine trusted_ips). Add your office and home addresses. If your home internet has IPv6, add your IPv6 range too, otherwise you’ll lock yourself out the moment your computer prefers IPv6. (More on Lists in my post How to use Lists in Cloudflare.) The Free plan gives you one custom list, so this is a good use for it.

Cloudflare dashboard, Manage account, Configurations, Lists: an IP list called trusted_ips with three entries (an office address, a home address and a home IPv6 range).
A trusted_ips list. These are documentation addresses; yours would be your office and home connections.

Step 2: Create a custom WAF rule. In your domain go to Security → Security rules, and under Custom rules click Create rule. Click Edit expression and paste in:

(http.request.uri.path eq "/wp-login.php"
  or (starts_with(http.request.uri.path, "/wp-admin")
      and http.request.uri.path ne "/wp-admin/admin-ajax.php"))
and not ip.src in $trusted_ips

Set the action to Block and deploy.

Edit custom rule: the expression blocking /wp-login.php and /wp-admin except admin-ajax.php for anyone not in $trusted_ips, with the action set to Block.
The rule, with the expression from above and the action set to Block.

That’s it. Bots now get a Cloudflare block page, and your server never even hears about them.

Cloudflare block page: Sorry, you have been blocked. You are unable to access the site. Why have I been blocked, what can I do, and a Ray ID at the bottom.
What anyone not on the list gets on /wp-login.php.

The bit everyone gets wrong: admin-ajax.php

Notice the line that excludes /wp-admin/admin-ajax.php? Don’t skip it.

Despite living in the admin folder, admin-ajax.php is used by the public side of lots of websites. Elementor forms, WooCommerce carts, search boxes and “load more” buttons all post to it. Block it and your contact form quietly stops working for every visitor.

How do I know? Because it happened to us. I wrote the whole story up in Our contact form was silently rejecting customers.

What about xmlrpc.php?

xmlrpc.php is an old remote-publishing interface that bots love because it lets them try lots of passwords in a single request. Unless you use the WordPress mobile app or Jetpack, you probably don’t need it. Add it to the same rule:

(http.request.uri.path in {"/wp-login.php" "/xmlrpc.php"}
  or (starts_with(http.request.uri.path, "/wp-admin")
      and http.request.uri.path ne "/wp-admin/admin-ajax.php"))
and not ip.src in $trusted_ips

Testing it

  • From your trusted connection, log in as normal. It should just work.
  • Turn off Wi-Fi on your phone and browse to /wp-login.php over mobile data. You should get a Cloudflare block page.
  • Still on mobile data, submit your contact form. It should still go through.
  • Check Security → Analytics → Events in Cloudflare after a day. You’ll see exactly how much junk the rule is stopping.
A Security Events entry expanded: service Custom rules, action Block, the rule name, and the request details including host and path /wp-admin/plugins.php. IP address and network blurred.
A blocked request in Security Events: which rule caught it and the path it asked for. (IP address blurred.)

Working away from the office?

If you log in from all over the place, an IP list gets painful. Options:

  • Use a VPN with a fixed IP and add just that.
  • Use Cloudflare Access (part of Zero Trust, free for small teams) to put a proper login screen with your Microsoft or Google account in front of wp-login.php. If you do, apply it to the login page only, not the whole /wp-admin folder. Same admin-ajax.php trap.

Not on WordPress? Same problem

WordPress cops most of the bot traffic because it runs so much of the web, but it’s not special. Every platform has a login page at a well-known address, and the bots know all of them:

Platform Where the login lives
Joomla /administrator
Drupal /user/login
Magento / Adobe Commerce /admin, or a custom admin path
Ghost /ghost
phpMyAdmin /phpmyadmin, /pma and friends
cPanel / WHM, Plesk ports 2083 / 2087, 8443
Grafana, Jenkins, Portainer and other dashboards their own login page, often on a known port
Routers, NAS boxes, firewalls the device’s web admin page

The fix is exactly the same: only let trusted IPs reach the login. For a site behind Cloudflare, swap the paths in the rule above. For Joomla, for example:

starts_with(http.request.uri.path, "/administrator")
and not ip.src in $trusted_ips

Before you deploy, check whether the public side of your site uses anything in that path, the same way WordPress uses admin-ajax.php. Then run the same tests: log in from your trusted connection, try the login over mobile data, and submit your forms.

Control panels, phpMyAdmin and device admin pages usually shouldn’t be on the internet at all. Close those ports in your server or router firewall, and reach them over a VPN instead.

A quick test for any of them: if you can open your admin login from a coffee shop without connecting to anything first, so can everyone else.

One last thing

This rule only protects traffic that goes through Cloudflare. If someone knows your server’s real IP, they can go around it. Make sure your origin is locked down too.

Want this set up for you? Get in touch. It takes about ten minutes, and it’s one of the best-value security changes you can make.

Naming Conventions for Cloudflare WAF Rules (and Why Ours Needed Fixing) (Cloudflare WAF)

Naming Conventions for Cloudflare WAF Rules (and Why Ours Needed Fixing)

Is Your Host Running an End-of-Life PHP Version? (WordPress)

Is Your Host Running an End-of-Life PHP Version?

Backups That Actually Work: The 3-2-1 Rule for Small Business Websites (Backups)

Backups That Actually Work: The 3-2-1 Rule for Small Business Websites

Cloudflare Turnstile vs reCAPTCHA for Small Business Forms (Spam protection)

Cloudflare Turnstile vs reCAPTCHA for Small Business Forms

Our Contact Form Was Silently Rejecting Customers (Case study)

Our Contact Form Was Silently Rejecting Customers

PHP2Shell: What It Is, and How to Protect Your WordPress Site (Security alert)

PHP2Shell: What It Is, and How to Protect Your WordPress Site