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 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.
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. Cloudflare evaluates allow rules before block rules, so an explicit allow for known-good machine traffic protects it from everything downstream.
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, with a rate limit of roughly 5 requests per minute per IP.
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.
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.
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.orstaging.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); 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.
- Log into
/wp-admin/ - Edit and save a post in the block editor, which exercises the REST API
- Use a front-end feature that calls
admin-ajax.php, such as a product filter - Complete a test checkout end to end
- Trigger a webhook from your payment provider’s dashboard and confirm it arrived
- Request a password reset and confirm the email arrives
- Load the site from a mobile network, not office wifi, which is where challenges appear
- 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.