Cloudflare security rules that can break WordPress

The usual sequence: security is raised, something breaks, the rules get switched off entirely. Here is what actually breaks, why, and the path-specific rules that avoid it.

The sequence is almost always the same.

Someone turns up the security settings, because the site is getting hit by bot traffic and the dashboard makes it look like a slider. Within a day, the admin area is slow or broken, a plugin stops updating, orders stop reaching the fulfilment system, or customers report a challenge page they cannot get past.

The rules get switched off. Now the site has no protection at all, and there is organizational resistance to trying again.

The problem is not that Cloudflare and WordPress are incompatible. It is that WordPress uses several endpoints in ways that look, to a generic rule, exactly like the attack traffic those rules exist to stop.

The short version

Six rules cover most WordPress sites. Four are custom rules, one is the rate limiting rule, one is a cache rule and the last lives on the server, so all of them fit the free plan’s limits of five custom rules and one rate limiting rule.

OrderWhat it protectsMatchesAction
1Payment and integration webhooksThe webhook path from your provider’s published rangesSkip remaining rules
2XML-RPC/xmlrpc.phpBlock, unless you use Jetpack
3The admin area/wp-admin/ except admin-ajax.php, from outside your addressesBlock
4LoginPOST to /wp-login.phpManaged Challenge, plus the rate limiting rule
5Personal pagesLogged-in, cart, checkout and account requestsBypass cache (a cache rule)
6The originAnything not arriving from CloudflareDropped at the server firewall

The details, and why each one is shaped the way it is, follow.

The four endpoints that cause almost all of it

admin-ajax.php

This is the biggest single source of trouble. WordPress and its plugins use admin-ajax.php for ordinary front-end functionality: filtering products, loading more posts, adding to a cart, validating a form field.

To a generic rule it looks alarming. Repeated POST requests to an admin path from a single address, at high frequency, is a textbook description of a login brute force attempt. It is also a textbook description of a customer using a faceted product filter.

If you rate limit /wp-admin/* as a block, you will throttle real customers on the front end who never go near the admin area.

In code you control, registering a REST route instead of an admin-ajax.php action gives each feature its own path that a rule can target. It is a small change in custom plugin code that makes every later rule simpler.

The REST API at /wp-json/

The block editor depends on it. The mobile app depends on it. Many plugins depend on it. Headless front ends depend on it entirely.

Rules that challenge /wp-json/ break editing before they break anything an attacker was doing. The symptom is distinctive: the editor loads but will not save, or saves and immediately shows “updating failed.”

There is a legitimate hardening measure here, which is restricting the endpoints that expose user enumeration, but that is surgical rather than blanket.

Webhook receivers

This is the one that costs money, because it fails silently.

A payment provider posts an order confirmation to your site. It is a machine. It cannot solve a challenge, it will not execute JavaScript, and it frequently sends no user agent worth speaking of. Bot management classifies it as automated traffic, because it is, and challenges it.

The provider’s request fails. Your site never learns the payment succeeded. The order sits unpaid or unconfirmed. Nothing errors visibly on your side, and you find out from a customer.

The login and checkout paths

wp-login.php genuinely does need protection, and it needs the right protection. A challenge that a customer on a mobile network cannot complete, applied to /checkout, is a self-inflicted outage on your most valuable page.

Rules that work

The principle throughout: be specific about paths, and allow before you block. Custom rules are evaluated in list order, and a Block or a challenge stops evaluation for that request. Put the skip rule for known-good machine traffic at the top of the list, so it matches before anything that could challenge it.

Allow your webhooks first

(http.request.uri.path eq "/wp-json/wc/v3/webhook"
  or http.request.uri.path contains "/?wc-api=")
and ip.src in {198.51.100.0/24 203.0.113.0/24}

Action: Skip all remaining rules, including bot management.

Get the source ranges from your payment provider’s documentation rather than guessing. If a provider does not publish ranges, use a shared secret in the path or verify the request signature at the application layer, and allow based on that instead.

Protect the login without touching the front end

http.request.uri.path eq "/wp-login.php"
and http.request.method eq "POST"

Action: Managed Challenge, plus a rate limiting rule on the same expression. On the free plan the rate limiting rule counts over a 10-second window, so something like 3 requests per 10 seconds per IP; on Pro and above, roughly 5 per minute. A person logging in sends one.

Note what this does not match: GET requests to the login page, so the form still loads normally, and nothing under /wp-admin/ that a customer might trigger.

Leave admin-ajax alone, or be precise about it

The safest position is not to rate limit admin-ajax.php at all, because you cannot distinguish front-end use from admin use by path.

If it is genuinely being abused, target the specific action rather than the file:

http.request.uri.path eq "/wp-admin/admin-ajax.php"
and http.request.body.raw contains "action=some_expensive_action"

That requires a plan that inspects the request body. Without it, exclude admin-ajax.php from your /wp-admin/* rules explicitly.

Block XML-RPC unless something needs it

xmlrpc.php is the oldest remote interface in WordPress, and a favourite of password guessers because one request can try many passwords. The WordPress version of this website received 3,370 POST requests to xmlrpc.php on 5 August 2026 alone.

http.request.uri.path eq "/xmlrpc.php"

Action: Block. Check first whether anything you rely on still uses it: Jetpack does, and some older publishing tools do. If Jetpack is in use, skip this rule for Jetpack’s published addresses instead of blocking outright.

Restrict the admin area by address, if you can

This is far stronger than any challenge, and available on any plan:

http.request.uri.path contains "/wp-admin/"
and not http.request.uri.path contains "admin-ajax.php"
and not ip.src in {203.0.113.0/24}

Action: Block.

The admin-ajax.php exclusion is what makes this safe. Without it you break the front end for everyone.

Never cache what belongs to a person

(http.cookie contains "wordpress_logged_in_"
  or http.cookie contains "woocommerce_items_in_cart"
  or http.cookie contains "wp-postpass_"
  or starts_with(http.request.uri.path, "/wp-admin")
  or starts_with(http.request.uri.path, "/cart")
  or starts_with(http.request.uri.path, "/checkout")
  or starts_with(http.request.uri.path, "/my-account"))

Action: Bypass cache.

This is the most important cache rule you will write. Serving a cached cart page to the wrong customer is a data incident, not a performance issue.

Create it as a cache rule, not a page rule. Page Rules are Cloudflare’s older mechanism and are being migrated to the newer rule types: cache behaviour to Cache Rules, security level to Configuration Rules, forwarding to Redirect Rules. Where both exist for the same behaviour, the newer rule takes precedence, so a forgotten page rule and a new cache rule can quietly disagree.

Bot Fight Mode cannot be skipped

This one catches out free-plan sites more than any rule above.

Bot Fight Mode, the free plan’s bot toggle, runs outside the custom rules pipeline. Cloudflare’s documentation is explicit that Skip, Bypass and Allow have no effect on it, so the webhook skip rule earlier on this page does nothing against it. It can also challenge API and mobile app traffic.

If payment webhooks, a mobile app or a monitoring service start failing after Bot Fight Mode is switched on, the choice is to turn it off and rely on the specific rules above, or to move to a plan with Super Bot Fight Mode, which does honour skip rules.

The step almost everyone skips

All of the above protects traffic that goes through Cloudflare. If your origin server still accepts connections from anywhere, none of it is mandatory from an attacker’s point of view. They connect to the origin IP directly and every rule above becomes decoration.

Finding an origin IP is usually trivial:

  • Historical DNS records are archived by several public services
  • Email sent from the server exposes it in the headers
  • Certificate transparency logs list subdomains
  • A mail. or staging. subdomain frequently points straight at the same machine, unproxied

Check whether yours is exposed:

# Does the origin answer directly for your hostname
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/ | head -3

If that returns your site rather than timing out, the origin is reachable.

The fix, at the origin:

# Allow web traffic only from Cloudflare, from the published ranges
for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
  ufw allow from "$ip" to any port 443 proto tcp comment 'cloudflare'
done
ufw deny 443/tcp

Automate refreshing that list. The ranges change occasionally, and a stale allow list eventually blocks legitimate edge traffic.

While you are there, check your DNS records for anything unproxied that points at the same server. A grey-clouded mail record is the most common leak.

Getting the visitor’s real IP

Once traffic arrives through Cloudflare, every request appears to come from a Cloudflare address. WordPress then sees one IP for everyone, which breaks rate limiting plugins, comment spam protection, security logging and any geolocation you rely on.

Cloudflare sends the original address in CF-Connecting-IP. Nginx has to be told to trust it:

# Restore the visitor IP, trusting only Cloudflare ranges
real_ip_header CF-Connecting-IP;
set_real_ip_from 173.245.48.0/20;
# ... the remaining published ranges

The set_real_ip_from entries are essential. Trusting a client-supplied header from any source lets anyone spoof their address by setting it themselves, which turns a logging fix into a security hole.

Deploy in log mode first

Every rule above should be created in a mode that logs rather than acts, where your plan supports it. Leave it for a day and read what it would have caught.

We have never done this without finding at least one surprise: a monitoring service, a partner integration, an accessibility tool, or a client’s own office traffic that would have been blocked.

A test list to run after every change

Not the homepage. Everyone tests the homepage.

  1. Log into /wp-admin/
  2. Edit and save a post in the block editor, which exercises the REST API
  3. Use a front-end feature that calls admin-ajax.php, such as a product filter
  4. Complete a test checkout end to end
  5. Trigger a webhook from your payment provider’s dashboard and confirm it arrived
  6. Request a password reset and confirm the email arrives
  7. Load the site from a mobile network, not office wifi, which is where challenges appear
  8. Confirm the origin still rejects direct connections

Why this is worth doing properly

Cloudflare in front of WordPress with default settings does something useful and considerably less than most people assume. Cloudflare with badly configured rules is worse than nothing, because the rules get disabled and the false sense of protection persists.

The difference between the two is not budget. Almost everything in this article works on the free plan. It is being specific about paths, allowing known-good machine traffic explicitly, testing the endpoints that break rather than the one that does not, and closing the origin so that the rules cannot simply be walked around. That combination is what a Cloudflare security setup should leave behind.

Frequently asked questions

What are the best Cloudflare settings for WordPress?

Path-specific rules rather than a high global security level: skip known webhooks first, block xmlrpc.php if nothing uses it, restrict /wp-admin/ by address while excluding admin-ajax.php, challenge POST requests to wp-login.php, bypass the cache for logged-in and cart traffic, and lock the origin to Cloudflare’s ranges.

Should I still use Page Rules for WordPress?

Not for new configuration. Cache behaviour belongs in Cache Rules, and security level in Configuration Rules. Existing page rules keep working while Cloudflare migrates them, but where a newer rule sets the same behaviour, the newer rule wins.

Why does Cloudflare block my WooCommerce or payment webhooks?

Usually because bot protection classifies the provider’s server as automated traffic, which it is. Add a skip rule for the webhook path and the provider’s published addresses at the top of your custom rules. If Bot Fight Mode is on, that skip rule has no effect, and Bot Fight Mode has to be turned off.

Does Cloudflare protect a WordPress site on its own?

Only for traffic that goes through it. If the origin server accepts connections from anywhere, an attacker can reach it directly and bypass every rule. Restricting ports 80 and 443 to Cloudflare’s published ranges closes that.

Dealing with this yourself?

If this describes a system you are responsible for, we can look at the specific case rather than the general one.