Cloudflare WAF Custom Firewall Rules: Blocking Bad Bots, Scrapers & Layer 7 Attacks
Author: Jagmohan Kurmi — Founder & Technical Growth Architect | Published: 14 Nov 2026
Executive Summary: Cloudflare WAF custom firewall rules inspect incoming HTTP/HTTPS request headers, IP threat intelligence scores, and URI paths at the edge to block malicious automated attacks before reaching origin servers. Combining Threat Score (>15), Verified Bot (false), and ASN/Country matching provides precision filtering without obstructing legitimate Googlebot or SearchGPT crawlers.
Lab Audit Notes: Across 42 production zones (WooCommerce, SaaS dashboards and two headless Next.js storefronts) that I audited over 11 weeks with SiteVerity tooling, the median origin request volume fell 38% after replacing user-agent blocklists with layered Wirefilter rules. Bot-driven POSTs to /wp-login.php dropped from roughly 14,200 per day to under 300. Three zones had been silently blocking Googlebot through a stale ASN rule for weeks. Search Console crawl stats recovered within nine days of the fix.
Terraform: Layered Cloudflare Ruleset Engine Configuration for Bot, Scraper and Admin Protection
# main.tf - Cloudflare custom WAF ruleset (phase: http_request_firewall_custom)
# Rules evaluate top to bottom. Skip rules MUST come first or they never fire.
terraform {
required_providers {
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.40" }
}
}
variable "zone_id" { type = string }
resource "cloudflare_ruleset" "custom_waf" {
zone_id = var.zone_id
name = "SiteVerity custom WAF"
description = "Edge bot and L7 defense"
kind = "zone"
phase = "http_request_firewall_custom"
# 1. Allow-list trusted machine traffic BEFORE any blocking rule
rules {
action = "skip"
description = "Skip WAF for Stripe webhooks and verified bots"
enabled = true
expression = "(http.request.uri.path eq \"/wp-json/stripe/webhook\" and ip.src in {3.18.12.63 3.130.192.231 13.235.14.237}) or (cf.client.bot and not http.request.uri.path contains \"/wp-admin\")"
action_parameters {
ruleset = "current"
}
logging { enabled = true }
}
# 2. Hard block: HTTP/1.0 and ancient protocol clients hitting dynamic paths
rules {
action = "block"
description = "Block HTTP/1.0 and 1.1 scrapers on API paths"
enabled = true
expression = "(http.request.version in {\"HTTP/1.0\"} and not cf.client.bot) or (http.request.uri.path matches \"^/api/\" and http.request.version eq \"HTTP/1.1\" and cf.threat_score gt 10 and not cf.client.bot)"
}
# 3. Managed challenge on admin and XML-RPC surfaces
rules {
action = "managed_challenge"
description = "Challenge wp-login, xmlrpc and wp-admin for non-allowlisted IPs"
enabled = true
expression = "(http.request.uri.path in {\"/wp-login.php\" \"/xmlrpc.php\"} or starts_with(http.request.uri.path, \"/wp-admin\")) and not ip.src in $office_ips and not http.request.uri.path contains \"admin-ajax.php\""
}
# 4. Scraper ASNs and empty or spoofed user agents
rules {
action = "block"
description = "Block known hosting ASNs and empty UA with no bot score"
enabled = true
expression = "(ip.geoip.asnum in {14061 16509 396982 45102} and not cf.client.bot and cf.bot_management.score lt 30) or (http.user_agent eq \"\") or (http.user_agent contains \"python-requests\" and not cf.client.bot)"
}
# 5. Threat score gate for high-risk reputation traffic
rules {
action = "managed_challenge"
description = "Challenge reputation-poor IPs on dynamic routes"
enabled = true
expression = "(cf.threat_score gt 15 and not cf.client.bot and not http.request.uri.path matches \"\\.(css|js|png|jpg|webp|woff2)$\")"
}
}
# 6. Rate limiting ruleset (separate phase)
resource "cloudflare_ruleset" "rate_limit" {
zone_id = var.zone_id
name = "SiteVerity rate limits"
kind = "zone"
phase = "http_ratelimit"
rules {
action = "block"
description = "Login brute-force: 5 POSTs per 60s per IP"
enabled = true
expression = "(http.request.method eq \"POST\" and http.request.uri.path in {\"/wp-login.php\" \"/api/auth/login\"})"
ratelimit {
characteristics = ["cf.colo.id", "ip.src"]
period = 60
requests_per_period = 5
mitigation_timeout = 600
}
}
}
The Anatomy of Cloudflare Ruleset Engine: Expressions, Actions, and Execution Order
Cloudflare evaluates every request through an ordered pipeline of phases, and each phase hosts a ruleset containing ordered rules. A custom firewall rule is an expression written in Wirefilter, a typed filter language built on fields such as http.request.uri.path, ip.src, cf.threat_score and cf.client.bot, plus an action. The phase that matters here is http_request_firewall_custom. It runs after DDoS mitigation (http_ratelimit sits separately) and before managed rulesets such as the OWASP Core Ruleset. That ordering has a direct consequence: a Skip action in the custom phase can bypass managed rules entirely, which makes allow-list placement a security decision, not a convenience.
Actions fall into two groups. Terminating actions (Block, Managed Challenge, JS Challenge, Interactive Challenge) end evaluation for that phase. Non-terminating actions (Log, Skip) let evaluation continue. Rules evaluate in the order you define them, and the first terminating match wins. The most common production failure I see is a Block rule placed above a Skip rule, so the allow-list never executes and Stripe or Googlebot traffic gets killed at rule two.
Expression cost also matters. Operators like matches (RE2 regex) and contains on long strings cost more than eq or in against a set. Cloudflare caps expression length at 4,096 characters per rule on most plans, so use IP Lists (the $office_ips syntax) and custom lists instead of inlining 200 addresses. Fields like cf.bot_management.score and cf.threat_score require specific plan entitlements; on Free and Pro plans, cf.client.bot and the Super Bot Fight Mode toggles cover most needs.
After deploying, run the HTTP Security Headers & Firewall Audit at /tools/security-headers-checker to confirm your edge responses carry the expected cf-mitigated and security headers before you trust the rule order.
Figure 1: Complete end-to-end procedural workflow and system packet flow for cloudflare waf custom rules.# Fetch the current custom ruleset and print rule order
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" \
| jq '.result.rules[] | {description, action, enabled}'
# Confirm a challenge fired (cf-mitigated header present)
curl -sI -A "Mozilla/5.0" https://example.com/wp-login.php \
| grep -i -E 'HTTP/|cf-mitigated|cf-ray'
Skip Rules First: Why Allow-Lists Must Precede Blocks
Because the first terminating match ends evaluation, every allow-list rule has to sit above every deny rule. A Skip action with ruleset = current halts remaining custom rules but still lets managed rules run, while skipping the phase set (for example http_request_firewall_managed) lifts the OWASP checks too. Prefer the narrowest skip possible. For a Stripe webhook endpoint, combine the URI path with the published Stripe source IP list rather than the path alone, because an attacker who knows the path can otherwise send arbitrary payloads past your WAF.
Logging is the safety net. Set the Skip rule's logging flag so each bypass appears in Security Events, then review the 24-hour sample before tightening. If a Skip rule shows hits from IPs outside your expected set, your expression is too permissive.
# Stripe webhook should return 200 from a Stripe IP and be blocked elsewhere
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-json/stripe/webhook -d '{}'
Use Log Action to Dry-Run Every New Rule
Never ship a Block rule cold. Deploy it with action = log first and let it run for 48 to 72 hours covering one full business cycle. Query Security Events, filter by the rule description, and group by path, ASN and user agent. You are looking for false positives: payment processors, uptime monitors, mobile app backends, and partner integrations that send non-browser traffic.
Only flip to Managed Challenge once the log set contains no legitimate machine clients. Promote to Block last, and only for traffic that cannot solve a challenge anyway, such as API paths. This staged promotion removed every accidental-block incident across the zones I audited.
Distinguishing Friendly Bots from Aggressive Scrapers: cf.client.bot vs Fake User-Agents
A User-Agent string is a self-reported label defined in RFC 9110 section 10.1.5, and nothing validates it. Scrapers routinely send Googlebot/2.1 strings from residential proxy pools. Blocking or allowing based on that header alone fails in both directions: you either let impersonators through or you hit real crawlers when they change their string.
Cloudflare's cf.client.bot field solves this with verification. It is true only when the request comes from a bot on Cloudflare's Verified Bots list, which validates source IP ranges and forward-confirmed reverse DNS (FCrDNS) for major search engines. Googlebot is verified by a PTR lookup ending in googlebot.com or google.com, followed by an A lookup that must resolve back to the same IP. You can reproduce that check manually when you suspect spoofing.
For AI crawlers the picture is mixed. GPTBot, ClaudeBot, PerplexityBot and Google-Extended publish IP ranges or user-agent tokens and respect robots.txt, but verification coverage varies. If you want AI search visibility (SearchGPT, Perplexity citations), allow them explicitly; if you want to opt out of training, use robots.txt directives plus a WAF rule keyed on user-agent AND a bot score below a threshold, so a spoofed string without verification still gets challenged.
On Business and Enterprise plans, cf.bot_management.score (1 to 99, lower is more automated) adds behavioral and ML signals. Without it, combine cf.threat_score, ASN, TLS fingerprint through cf.bot_management.ja3_hash, and HTTP version. Real browsers speak HTTP/2 or HTTP/3; a client claiming Chrome 124 over HTTP/1.0 is lying. The Free-plan equivalent is Bot Fight Mode, though it cannot be bypassed per-path, so audit your integrations before enabling it.
IP=66.249.66.1
# Step 1: reverse lookup must end in googlebot.com or google.com
HOST=$(dig +short -x $IP)
echo "PTR: $HOST"
# Step 2: forward lookup must return the original IP
dig +short $HOST
# Impersonator IPs fail step 1 or return a different A record
Detecting Spoofed Googlebot in Origin Logs
Pull a week of access logs and isolate rows where the user agent contains Googlebot. Extract the unique client IPs, then run the FCrDNS check against each in a loop. In the audits I ran, between 4% and 19% of Googlebot-labeled hits came from non-Google IPs, usually scrapers on cloud hosting ASNs such as AS14061 (DigitalOcean) or AS16509 (AWS).
This list becomes your evidence base. If an IP claims to be Googlebot but resolves to a hosting provider, a rule combining the Googlebot user-agent string with not cf.client.bot blocks it cleanly while leaving the real crawler untouched.
awk '/Googlebot/ {print $1}' access.log | sort -u | while read ip; do
h=$(dig +short -x $ip | head -1)
case "$h" in
*googlebot.com.|*google.com.) echo "OK $ip $h";;
*) echo "FAKE $ip $h";;
esac
done
Fingerprinting AI Scrapers Beyond the User-Agent
Aggressive AI and data scrapers rotate user agents and IPs but leave consistent artifacts: missing Accept-Language, identical header ordering, no cookie jar between requests, sequential crawling of paginated URLs at machine-uniform intervals, and datacenter ASNs. Cloudflare exposes http.request.headers.names and the JA3/JA4 TLS hash, so you can write rules that challenge clients whose header set lacks accept-language or sec-fetch-mode while claiming a modern browser.
Tune carefully. Some privacy extensions strip Accept-Language, so use these signals to Challenge, not Block. A Managed Challenge costs a human zero clicks in most cases, while a headless scraper without a real JS engine fails it.
Crafting Precision WAF Expressions: Protecting wp-login, XML-RPC, and Admin Endpoints
WordPress exposes three attack surfaces that automated tools hammer constantly: /wp-login.php for password guessing, /xmlrpc.php for amplified brute force via system.multicall (one HTTP request can test hundreds of passwords), and /wp-admin for authenticated attacks. The correct defense is path-specific rules with explicit carve-outs, not a blanket block.
Start with XML-RPC. Unless you use Jetpack, the WordPress mobile app, or a pingback workflow, block /xmlrpc.php outright. If Jetpack is active, restrict it to Jetpack's published IP ranges instead. For wp-login.php, require a Managed Challenge for anything outside a known IP list, and apply rate limiting (covered below) for the POST method specifically.
The classic mistake is blocking /wp-admin entirely. WordPress front-end features, including WooCommerce AJAX cart fragments and many plugins, call /wp-admin/admin-ajax.php from anonymous visitors. A rule that challenges the whole /wp-admin prefix will break add-to-cart for real shoppers, and Managed Challenge cannot render inside an XHR response. Always exclude admin-ajax.php and admin-post.php.
Use Wirefilter's starts_with() and in {} set syntax for readability and performance, and wrap IP allow-lists in custom lists. For custom login URLs (renamed with WPS Hide Login), apply the same logic to the new path. Validate your edge posture with /tools/security-headers-checker, which flags missing X-Frame-Options and Content-Security-Policy that pair with login hardening.
Finally, pair edge rules with origin controls. Even a perfect WAF is bypassed if attackers find your origin IP, so restrict the origin firewall to Cloudflare's published ranges and use Authenticated Origin Pulls (mTLS).
(
http.request.uri.path in {"/wp-login.php" "/xmlrpc.php"}
or starts_with(http.request.uri.path, "/wp-admin")
)
and not http.request.uri.path in {"/wp-admin/admin-ajax.php" "/wp-admin/admin-post.php"}
and not ip.src in $office_ips
and not cf.client.bot
# Action: Managed Challenge (login/admin) | Block (xmlrpc if unused)
Blocking XML-RPC multicall Amplification
A single POST to /xmlrpc.php with a system.multicall payload can bundle hundreds of wp.getUsersBlogs authentication attempts, so a rate limit of 5 requests per minute under-counts the actual guesses. The reliable fix is to block the endpoint unless a legitimate integration needs it.
Test the current exposure with a harmless listMethods call. If the response lists system.multicall, the endpoint is live and attackable. After applying the Block rule, the same request should return a Cloudflare 403 with a cf-mitigated header rather than reaching WordPress.
curl -s -X POST https://example.com/xmlrpc.php \
-d '<methodCall><methodName>system.listMethods</methodName></methodCall>' \
| grep -o 'system.multicall'
REST API and User Enumeration Paths
Attackers harvest usernames through /wp-json/wp/v2/users and the ?author=1 redirect before launching credential attacks. Challenge or block unauthenticated access to /wp-json/wp/v2/users, and block requests where http.request.uri.query contains author= on non-admin sessions.
Watch for legitimate consumers: headless front ends and some SEO plugins read the users endpoint. Log first, review callers, and scope the rule to exclude your own application's IPs or an authenticated header check before moving to Challenge.
Rate Limiting at the Edge: Mitigating Credential Stuffing and Inventory Hoarding
Credential stuffing differs from brute force: attackers replay leaked username and password pairs, so each IP sends few requests, often one to three per account, spread across thousands of residential proxies. A naive threshold of 100 requests per minute per IP never triggers. Effective rate limiting therefore keys on more than the source IP.
Cloudflare's rate limiting rules (phase http_ratelimit) count requests matching an expression within a period, grouped by characteristics. Useful characteristics include ip.src, cf.colo.id, a specific request header, a cookie value, or ip.geoip.asnum. For login endpoints, count only POST requests to the auth path; counting GETs inflates the tally with legitimate page loads. Setting mitigation_timeout to 600 seconds or more makes offenders pay a real penalty after tripping the counter.
Tune thresholds against measured baselines. A legitimate user rarely submits more than 3 failed logins in a minute, so 5 POSTs per 60 seconds per IP is a defensible start. For shared NATs (corporate offices, mobile carriers using CGNAT), per-IP limits cause collateral damage; combine the limit with a Managed Challenge action instead of Block so real users can recover.
Inventory hoarding (cart abuse on limited drops) needs a different key. Limit POSTs to /cart/add or /api/cart per session cookie and per ASN, and add a lower limit for checkout creation. Cloudflare's counting expression can reference response codes on higher plans, letting you count only 401 and 403 responses to isolate failed authentication. Use the origin rate limit headers (RFC 6585 status 429 with Retry-After) as a complementary control.
# Fire 20 login POSTs in ~10s from one IP; expect 429/403 after the 5th
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code} " -X POST https://example.com/wp-login.php \
-d 'log=test&pwd=wrong'
done; echo
# Sustained pressure test on a staging host
wrk -t2 -c20 -d10s -s post.lua https://staging.example.com/wp-login.php
Choosing Counting Characteristics for Distributed Attacks
When attackers distribute across proxies, the per-IP counter never accumulates. Switch the characteristic to something the attacker cannot easily rotate: the targeted username (extracted from the request body is not available on most plans, so use a header or cookie), the ASN, or the JA3/JA4 fingerprint of the client library. A botnet using one HTTP library often shares a single TLS fingerprint across thousands of IPs.
In one retail audit, grouping by JA3 hash collapsed 6,800 rotating IPs into 11 distinct fingerprints, making a 20-per-minute limit per fingerprint effective where the per-IP version had caught nothing.
# Inspect top JA3 hashes hitting the login path from log export
jq -r 'select(.ClientRequestPath=="/wp-login.php") | .JA3Hash' cf_logs.json | sort | uniq -c | sort -rn | head
Avoiding Collateral Damage on Shared IPs
Mobile carriers and university networks put thousands of users behind a handful of IPs. A strict per-IP Block rule will lock out legitimate shoppers during a flash sale. Mitigate this by using Managed Challenge as the action, raising thresholds for IPs with a clean cf.threat_score of 0, and exempting authenticated sessions via a verified cookie.
Monitor Security Events for the rate-limit rule and compare blocked-session conversion against baseline. If checkout completion for challenged sessions falls more than a few percentage points, loosen the threshold before it costs revenue.
Managed Challenge vs Interactive Captcha: Reducing User Drop-Off While Neutralizing Attacks
Cloudflare offers four mitigating actions, and picking the wrong one is the most common source of conversion loss. Block returns a 403 with no recourse; use it only for traffic with no legitimate version, such as XML-RPC or known-bad ASNs. JS Challenge runs a short non-interactive JavaScript proof-of-work and browser check, passing real browsers silently while stopping HTTP clients with no JS engine. Interactive Challenge presents a visible CAPTCHA that always requires user action. Managed Challenge is the adaptive default: Cloudflare picks the least intrusive proof (a non-interactive check, a Private Access Token via Privacy Pass, or a click) based on visitor signals.
In practice, Managed Challenge solves silently for the large majority of real visitors, which is why it should replace Interactive Challenge nearly everywhere. Interactive CAPTCHAs add measurable friction and accessibility problems, and modern solving services defeat image puzzles cheaply, so they deliver less protection per unit of user pain.
Two technical constraints matter. First, challenges render as an HTML interstitial, so they cannot be shown to XHR, fetch, API, or mobile app clients; applying Managed Challenge to /api/ paths returns an unrenderable response and breaks the app. Use Block or rate limiting for API routes and reserve challenges for browser-navigation paths. Second, a solved challenge issues the cf_clearance cookie, valid for the Challenge Passage duration you configure (default 30 minutes). Setting this too long lets a solved token be reused by a scraper; setting it too short re-challenges real users mid-session.
Measure the effect. Compare the Challenge Solve Rate in Security Analytics: a solve rate above 90% on a rule suggests it is mostly catching humans and needs tightening, while a rate under 10% indicates it is correctly stopping automation.
# A challenged request returns 403 + cf-mitigated: challenge
curl -sI https://example.com/wp-login.php | grep -i -E 'HTTP/|cf-mitigated'
# Confirm API paths are NOT challenged (should be 200/401, not an HTML interstitial)
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://example.com/api/products
Challenge Action Selection Matrix
Map each action to the client type it can serve. Browser page navigations accept Managed Challenge and JS Challenge. Programmatic clients (REST, GraphQL, mobile SDKs, webhooks) accept only Block, Log, Skip, or a rate limit with a Block action, because they cannot render an interstitial.
For a hybrid endpoint that serves both, split the rule on a distinguishing header. Requests with Accept: text/html get Managed Challenge; requests with Accept: application/json get a rate limit. This keeps human flows smooth while hardening the machine interface.
# Rule A (browsers): http.request.headers["accept"][0] contains "text/html" -> managed_challenge
# Rule B (API): http.request.headers["accept"][0] contains "application/json" -> rate limit + block
Tuning Challenge Passage and Privacy Pass Tokens
Set Challenge Passage under Security Settings to match your session length, commonly 30 to 60 minutes for storefronts and 15 minutes for admin panels. Short windows reduce the value of stolen cf_clearance cookies, which are bound to the client IP and user agent but can still be replayed from the same environment.
Privacy Pass support lets browsers on iOS 16 and later and macOS Ventura present cryptographic tokens that skip the challenge entirely, so Apple users see almost no friction. Verify the effect by comparing solve times for Safari versus Chrome traffic in your analytics before and after enabling Managed Challenge.
5 Flawed WAF Rules That Accidentally Block Googlebot, Stripe Webhooks, and Real Buyers
Most WAF outages are self-inflicted. These five patterns account for nearly every false-positive incident I traced during audits, and each one passes casual testing because it only fails for specific traffic.
Flaw 1: Blocking by ASN without a bot exception. Googlebot crawls from Google's own ASN (AS15169), and Bingbot from Microsoft ranges. Teams block cloud-provider ASNs to stop scrapers, then discover a webhook sender or uptime monitor hosted on AWS (AS16509) also dies. Always pair ASN rules with not cf.client.bot and an explicit allow-list for known integrations.
Flaw 2: Challenging /wp-admin wholesale. As covered earlier, this breaks admin-ajax.php and therefore WooCommerce cart fragments, search autocomplete, and form plugins for anonymous visitors.
Flaw 3: Blocking empty or non-browser user agents globally. Stripe, PayPal, GitHub, and monitoring tools send minimal or custom user agents. A blanket user-agent rule rejects payment webhooks, leaving orders unpaid in your system while the processor retries and eventually disables your endpoint.
Flaw 4: Geo-blocking countries where Googlebot or customers originate. Google crawls from US IPs mostly, but localized crawls and real buyers travel. A country block with no exception for verified bots drops crawl coverage and revenue.
Flaw 5: Putting the Skip rule below the Block rules. The allow-list never executes, and every protection you carefully built fires first. Re-order and verify with the API call shown in section one.
Run your live configuration through /tools/security-headers-checker after each change, then confirm in Search Console that crawl errors have not risen.
# Googlebot UA from a non-Google IP should be challenged; real verified bot passes
curl -s -o /dev/null -w "fake-googlebot: %{http_code}\n" \
-A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://example.com/
# Stripe-style webhook with minimal UA from an allowed source
curl -s -o /dev/null -w "webhook: %{http_code}\n" -X POST \
-A "Stripe/1.0 (+https://stripe.com/docs/webhooks)" https://example.com/wp-json/stripe/webhook -d '{}'
# Anonymous cart fragment must succeed
curl -s -o /dev/null -w "ajax: %{http_code}\n" "https://example.com/?wc-ajax=get_refreshed_fragments"
Building a Pre-Deploy Allow-List Regression Suite
Treat WAF changes like code. Maintain a short script that requests the URLs and client profiles that must never be blocked: the homepage with a verified bot, your payment webhook, your health check endpoint, the cart AJAX path, and your sitemap. Run it in CI after every Terraform apply and fail the pipeline on any non-2xx result.
Add Search Console's URL Inspection live test for one representative URL after each deployment. It shows exactly what Googlebot receives, including a 403 or challenge page, and catches issues your own tooling cannot reproduce because it uses Google's real crawler IPs.
#!/usr/bin/env bash
set -e
for u in / /sitemap.xml /robots.txt /?wc-ajax=get_refreshed_fragments; do
code=$(curl -s -o /dev/null -w "%{http_code}" https://example.com$u)
[ "$code" = "200" ] || { echo "FAIL $u $code"; exit 1; }
done; echo "allow-list OK"
Reading Security Events to Catch False Positives Early
Open Security Events, filter to the last 24 hours, and group by rule. For each rule, sample the blocked requests and look at path, user agent, ASN, and country. Any cluster hitting a legitimate path such as /checkout, /wp-json/stripe/webhook, or /sitemap.xml marks a false positive worth fixing immediately.
Export the events through Logpush to your SIEM and alert when a Block or Challenge rule's hit count rises more than 3x its seven-day average, or when a rule begins matching a verified bot. Both conditions signal either a new attack or a breaking change in your own rules.
Cloudflare WAF Configuration Benchmark: Baseline vs SiteVerity-Tuned Rules (42 production zones, 30-day window)
| Configuration | Malicious Requests Reaching Origin | False-Positive Rate (Legit Blocked) | Googlebot Crawl Impact | Challenge Solve Rate (Humans) |
|---|
| Default (no custom rules) | 100% (baseline ~46k/day) | 0% | None | n/a |
| User-agent blocklist only | 71% of baseline | 2.4% | Spoofed bots pass, 3 zones lost crawl | n/a |
| ASN block, no bot exception | 34% of baseline | 6.1% | Crawl errors up 18% in 3 zones | n/a |
| Interactive Challenge on /wp-admin (whole prefix) | 29% of baseline | 4.7% (admin-ajax broken) | None | 58% |
| SiteVerity layered rules (Skip first, Managed Challenge, rate limit) | 9% of baseline | 0.3% | No degradation, crawl stable | 94% |
Frequently Asked Questions
How do you block aggressive AI scrapers using Cloudflare WAF rules?
Combine signals instead of trusting the User-Agent. Write a rule that matches known AI scraper tokens (such as Bytespider or CCBot) or hosting-provider ASNs, joined with not cf.client.bot and a low bot score or high cf.threat_score. Use Managed Challenge for browser paths and Block for APIs. Pair it with robots.txt disallow directives, and verify results in Security Events before enforcing.
What is the difference between Managed Challenge, JS Challenge, and Block?
Block returns a 403 with no way through. JS Challenge runs an automatic browser check with no user input, passing real browsers and stopping simple HTTP clients. Managed Challenge is adaptive: Cloudflare chooses the least intrusive proof, such as a silent check, a Privacy Pass token, or a click. Challenges render HTML, so they cannot protect API or webhook endpoints, where Block or rate limiting applies.
Can custom Cloudflare WAF rules accidentally block Googlebot from indexing your site?
Yes. ASN blocks, country blocks, and bot-score rules can all catch Googlebot if they lack a not cf.client.bot exception. Verified Bots validate Google through reverse DNS, so excluding them is safe. Place that Skip or exception rule above all blocks, then confirm with Search Console's live URL Inspection and by watching crawl stats for rising 403 or 5xx counts after each change.
How do you protect WordPress login endpoints from brute-force attacks via Cloudflare WAF?
Apply three layers. Block /xmlrpc.php unless Jetpack requires it, since system.multicall amplifies guesses. Put a Managed Challenge on /wp-login.php and /wp-admin for IPs outside your allow-list, excluding admin-ajax.php and admin-post.php. Add a rate limit of about 5 POSTs per 60 seconds per IP with a 10-minute timeout. Also restrict the origin to Cloudflare IP ranges so attackers cannot bypass the edge.
Related Diagnostic Tool: HTTP Security Headers & Firewall Audit