Case 001. Cloudflare Crawler Surface Policy: Controlling Bot Access to URLs

The Great and Terrible Cloudflare series. Cloudflare Operations — policy, security and fleet management.

How can a store close transactional URLs to crawlers, retain access to public content, and verify the result across a fleet of sites? This case describes a solution that developed from a particular production incident and client infrastructure.

According to the author’s account, the fleet covered 22 sites with a combined daily audience of approximately 400,000 unique users. These figures provide author-supplied context; the aggregate metrics and pilot results below have not undergone an independent audit. Client domains, credentials, and private snapshots are excluded from publication.

The management tools initially ran on the client’s shared hosting and were written in PHP: the environment already existed alongside the sites, and the operator needed no separate server. A new independent installation can use a local environment such as WSL. The architectural principle remains consistent: configuration and policy have a reviewable state, secrets and private overlays stay local, and Cloudflare changes pass through a separate controlled write path.

Standalone code and examples: IRONCREED/cloudflare, under Case 001. Every instruction below uses that repository’s structure.

A useful bot on a transactional surface

On a WooCommerce site, crawler traffic repeatedly requested routes and parameters serving the cart, checkout, and customer account:

/cart/
/checkout/
/my-account/
?add-to-cart=
?remove_item=
?wc-ajax=

These requests can invoke PHP, WooCommerce, sessions, and cart operations. Meta-WebIndexer was one conspicuous participant. Globally blocking that User-Agent would also remove its access to product pages and articles that may have search and linking value.

comparisonFrom incident to policy

Observed load opens two options: a global identity block or a restriction on a particular surface; this case selects the latter.

  • source: Crawler on transactional URLs
  • boundary: Global identity block
  • state: Surface restriction
  • state: Public content outside the rule
  • incident references global
  • incident derives surface
  • surface produces content
Observed load opens two options: a global identity block or a restriction on a particular surface; this case selects the latter.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g01 · authorial model

The decision unit becomes crawler identity × request surface × profile × zone configuration.

flowDecision components

Identity, URL class, profile, and local conditions converge in the zone’s effective policy.

  • state: Crawler identity
  • state: Request surface
  • state: Profile
  • state: Zone conditions
  • artifact: Effective policy
  • identity requires effective
  • surface requires effective
  • profile requires effective
  • zone requires effective
Identity, URL class, profile, and local conditions converge in the zone’s effective policy.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g02 · authorial model

Three URL classes

SurfaceExamplesDecision in this case
Public contentHomepage, product, category, articleThis policy leaves the request outside its block rule
Transactional hard-denyCart, checkout, account, operational query parametersThe selected crawler class is blocked at the edge
CombinatorialFilters, faceted navigation, internal search, parameter combinationsHostname-specific SEO review before a policy change

The example paths match /cart, /checkout, /my-account, and descendants with a / after the name. A similar prefix such as /cartography is outside that list. Translated WooCommerce slugs, language prefixes, URL encoding, and query behaviour require testing on each actual site.

The demonstration rule searches for add-to-cart=, remove_item=, and wc-ajax= as query substrings. This simple example may also match another parameter’s value. Its matching precision needs review before production use. Filters and search remain outside the shared example: a useful SEO landing page on one store may resemble an unnecessary combination on another.

Identity and scope

Using all of cf.client.bot as this case’s crawler list would be too broad. Verified automation also includes monitoring, security checks, and other services. The demonstration uses narrower cf.verified_bot_category values:

Search Engine Crawler
AI Search
AI Crawler
Page Preview

It adds User-Agent fallbacks for Meta-WebIndexer, Googlebot, and Bingbot. These strings can be spoofed: a fallback classifies a request but does not authenticate its origin. As checked on 6 September 2026, Cloudflare retains these legacy categories for WAF compatibility, while its new taxonomy classifies bot behaviour differently. Verified bots

gateIdentity and surface

A request is blocked only when both the selected crawler identity and the transactional surface match.

  • state: Crawler identity match
  • state: Transactional surface match
  • decision: Both conditions
  • boundary: Block at edge
  • state: Outside this block rule
  • identity requires and
  • surface requires and
  • and rejects block: Yes
  • and returns other: No
A request is blocked only when both the selected crawler identity and the transactional surface match.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g03 · authorial model
RequestResult of Crawler Surface Policy itself
Meta-WebIndexer → /Not blocked by this rule
Meta-WebIndexer → /product/...Not blocked by this rule
Meta-WebIndexer → /cart/Block
Meta-WebIndexer → /checkout/Block

Other Cloudflare rules and the origin can change the final response. Failure to match one block rule does not create a global allow.

Why the edge

The restriction could be implemented in WordPress, Apache, nginx, or the application. Choosing Cloudflare places rejection before the origin: a crawler matching the defined transactional surface should not invoke WooCommerce. Custom Rules execute in their own ruleset-engine phase. Cloudflare Custom Rules

The initial production model combined a global baseline, site profile, and hostname exceptions. The portable tool generalises that model through explicit policy composition.

flowFrom profile to effective state

Profile policies, additional policies, disabled policies, and inline rules determine logical rules; the compiler turns them into physical rules.

  • state: Profile policies
  • state: Additional policies
  • boundary: Disabled policies
  • state: Inline rules
  • state: Logical rules
  • state: Physical rules
  • artifact: Effective managed state
  • profile produces logical
  • additional produces logical
  • disabled rejects logical: Remove from composition
  • inline produces logical
  • logical derives physical
  • physical produces state
Profile policies, additional policies, disabled policies, and inline rules determine logical rules; the compiler turns them into physical rules.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g04 · authorial model

From production workspace to a standalone repository

The original shared-hosting workspace contained policies, profiles, exceptions, scripts, backups, and reports. It produced the compiler, diff, dry-run, audit, deployment, snapshots, and edge smoke. The standalone repository contains the reusable mechanism, fixtures, and demonstration configurations. Private client overlays remain outside it.

Path in IRONCREED/cloudflarePurpose
CONSTITUTION.md, governance/PROFILE.mdAdopted normative source and release-candidate profile
LICENSE.mdLicensing map for code, prose, and branding
code-constitution/Normative-corpus submodule pinned to a commit
repository-licensing-policy/Licensing-policy submodule pinned to a commit
cases/001-crawler-surface-policy/bin/compile, plan, apply, audit, export, rollout
cases/001-crawler-surface-policy/src/PHP API client, compiler, and Ruleset Manager
cases/001-crawler-surface-policy/examples/Inventory, profiles, policies, and zone fixtures
cases/001-crawler-surface-policy/tests/Offline checks without Cloudflare tokens
cases/001-crawler-surface-policy/runtime/Local output excluded from Git

The relocation preserves the case ID, licences, and exact submodule revisions. A provenance record identifies the source tree and subsequent changes. Repository preparation and automated checks leave the final release decision with a human.

Case 001’s boundaries

The tool manages zone-level WAF Custom Rules in http_request_firewall_custom. DNS, Workers, Zero Trust, cache rules, billing, account membership, and origin configuration remain outside this case.

flowCase 001 operating cycle

Explicit inventory leads to compilation, live-state inspection, a plan preserving the unmanaged portion, a separate operator decision, and result verification.

  • source: Explicit inventory
  • process: Compile
  • process: Read live phase
  • process: Preserve unmanaged
  • artifact: Plan / diff
  • human-decision: Operator decision
  • process: Apply and snapshots
  • process: Readback and audit
  • inventory derives compile
  • compile derives live
  • live derives preserve
  • preserve derives plan
  • plan derives review
  • review derives apply
  • apply derives verify
Explicit inventory leads to compilation, live-state inspection, a plan preserving the unmanaged portion, a separate operator decision, and result verification.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g05 · authorial model

The files bot-example.json, country-policy.json, crawler-surface.json, named-ip-list.example.json, and scanner-user-agents.json demonstrate the mechanism’s capabilities. For example, the commerce fixture additionally includes bot-example; reproducing the pilot’s surface-only restriction requires separately reviewing and, where appropriate, removing that extra policy. The examples are not a production baseline.

Inventory, profiles, and exceptions

The inventory explicitly lists zones, phase, ownership prefix, and budget:

{
	"schema_version": 1,
	"phase": "http_request_firewall_custom",
	"managed_prefix": "[CFOPS]",
	"max_physical_rules": 5,
	"zones": ["domain-01.example", "domain-02.example", "domain-03.example"]
}

5 is an example parameter tied to the pilot’s conditions. Check it against actual zone limits; the budget must also count unmanaged rules. Custom Rules availability

ProfilePolicies
defaultscanner-user-agents
commercescanner-user-agents, crawler-surface
restricted-geoscanner-user-agents, country-policy

A zone configuration selects a profile and modifies its composition:

{
	"zone": "domain-02.example",
	"profile": "commerce",
	"additional_policies": ["bot-example"],
	"disabled_policies": []
}
topologyCommerce fixture

Two profile policies and one zone policy form three logical components.

  • source: Commerce profile
  • state: scanner-user-agents
  • state: crawler-surface
  • source: Zone additional_policies
  • state: bot-example
  • profile requires scanner
  • profile requires surface
  • additional produces bot
Two profile policies and one zone policy form three logical components.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g06 · authorial model

An inline rule can handle one hostname without creating another shared profile. The compiler also requires id and name:

{
	"zone": "domain-03.example",
	"profile": "restricted-geo",
	"inline_rules": [
		{
			"id": "forum-example",
			"name": "Forum hostname example",
			"action": "managed_challenge",
			"physical_group": "challenge",
			"expression": "(http.host eq \"forum.domain-03.example\" and ip.src.country ne \"UA\")"
		}
	]
}

The geographic restriction demonstrates an exception mechanism. Its suitability depends on the actual site’s audience.

Logical and physical rules

PolicyCompiler collects profile policies, adds additional_policies, removes disabled_policies, and appends inline_rules. Simple rules with the same action and physical_group are joined with or into one physical rule. Their logical files remain separate.

flowLogical → physical

Several logical block policies form one physical block rule; challenge remains a separate group.

  • state: Logical block policy A
  • state: Logical block policy B
  • state: Logical block policy C
  • artifact: One physical block
  • state: Logical challenge policy
  • artifact: Separate physical challenge
  • scanner derives block
  • surface derives block
  • bot derives block
  • challenge derives challenge-out
Several logical block policies form one physical block rule; challenge remains a separate group.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g07 · authorial model
(crawler_surface_condition) or (bot_condition)

This is an explanatory formula, not a deployable Cloudflare expression. Grouping requires compatible actions and a review of execution order. The current compiler serves the demonstrated simple rules; it does not guarantee preservation of arbitrary action_parameters in managed policies.

The managed/unmanaged boundary

The tool owns only rules whose description begins with [CFOPS]. A new managed block replaces the first previous managed rule’s position; when none exists, it is appended. Unmanaged rules retain their relative order.

gateOwnership boundary

The prefix defines ownership; managed rules are replaced, while the remainder enters the plan with its relative order preserved.

  • source: Live rules
  • decision: Description starts with prefix?
  • process: Replace managed block
  • process: Preserve unmanaged relative order
  • artifact: Final plan
  • live references prefix
  • prefix executes managed: Yes
  • prefix returns unmanaged: No
  • managed produces plan
  • unmanaged produces plan
The prefix defines ownership; managed rules are replaced, while the remainder enters the plan with its relative order preserved.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g08 · authorial model

If managed and unmanaged rules were interleaved, collecting the managed portion into one block may change their positions relative to each other. Review the plan for that change. Another writer using the same prefix breaks the ownership boundary. Sequential rollout inside one process also provides no lock against simultaneous dashboard edits or another tool.

A local WSL environment

On Windows, WSL can be installed from PowerShell:

wsl --install -d Ubuntu

The command and prerequisites are described in Microsoft’s documentation. Inside Ubuntu, install Git, PHP 8.1+, and cURL:

sudo apt update
sudo apt install -y git php-cli php-curl
git clone https://github.com/IRONCREED/cloudflare.git
cd cloudflare
git submodule update --init --recursive
cd cases/001-crawler-surface-policy
php tests/run.php
cp -R examples local
export CFOPS_CONFIG_DIR="$PWD/local"
export CFOPS_RUNTIME_DIR="$PWD/runtime"

local/ is excluded from Git and holds real zones and private overlays. runtime/ holds snapshots and reports. Access to normative submodules follows their own repositories’ policies; offline engine tests do not require downloading them. Copy examples during the first installation, before editing local configuration.

Creating and storing API tokens

In Cloudflare, open your profile → API Tokens → Create Token → Create Custom Token. Read operations in this case require Zone / Zone / Read for zone-ID lookup and Zone / WAF / Read for rulesets. Give a separate write token Zone / Zone / Read and Zone / WAF / Edit; it is also used for inspection before and after writing. Restrict both tokens to the required zones and, where practical, an expiry and the operator’s IP. Check permission names and availability in the current interface. Create a token, permission reference

For a single Bash session, enter the read token without displaying it or placing its value in command history:

read -rsp 'Cloudflare read token: ' CLOUDFLARE_READ_TOKEN
printf '\n'
export CLOUDFLARE_READ_TOKEN

Enter the write token in the same way as CLOUDFLARE_WRITE_TOKEN immediately before an approved operation, then remove it from the environment:

read -rsp 'Cloudflare write token: ' CLOUDFLARE_WRITE_TOKEN
printf '\n'
export CLOUDFLARE_WRITE_TOKEN
# Run the reviewed apply command here.
unset CLOUDFLARE_WRITE_TOKEN

For reuse, use a secrets manager or a file outside the checkout, for example under ~/.config/ironcreed/cloudflare/, with directory permissions 700 and file permissions 600. Enter values through an editor or secrets manager; .gitignore cannot remove a secret already committed to Git history. Revoke and replace an exposed token. Environment variables are inherited by child processes, so load the write token only for the operation.

gateCredential separation

A read token supports observation; a write token together with an explicit decision opens the apply path.

  • source: Read token
  • process: Plan / audit / export
  • source: Write token
  • human-decision: Review and --apply
  • process: API write
  • read requires observe
  • write requires apply
  • human validates apply
A read token supports observation; a write token together with an explicit decision opens the apply path.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g09 · authorial model

Fixtures, compile, and plan

php tests/run.php checks the local compilation model without API tokens. These tests do not execute expressions at the Cloudflare edge. Compilation is also local:

php bin/compile.php --zone=domain-02.example

Plan uses the read token and an actual zone’s configuration. Replace example.com in the following commands with a zone from your own inventory:

php bin/plan.php --zone=example.com

Ruleset Manager resolves the zone ID, reads the phase entry point, separates managed rules, compiles desired state, constructs the final list, checks the budget, and identifies a change. It performs no write. Expression comparison preserves whitespace inside strings; a formatting difference can produce a conservative review signal.

Apply and evidence

php bin/apply.php --zone=example.com --apply

Without --apply, the CLI refuses before creating an API client. With that flag, it uses the separate write token. Before writing, the tool reads the current state again and checks the limit. An unchanged result returns NO_CHANGE.

gateWrite boundary

Plan and budget checks precede the snapshot; ruleset write is followed by readback, and a mismatch stops the operation.

  • process: Fresh inspection
  • decision: Within budget?
  • artifact: Before snapshot
  • process: Ruleset write
  • process: Readback / after snapshot
  • decision: Expected state?
  • state: Verified
  • boundary: Stop with error
  • inspect produces budget
  • budget validates snapshot: Yes
  • budget rejects stop: No
  • snapshot requires write
  • write produces readback
  • readback validates match
  • match validates success: Yes
  • match rejects stop: No
Plan and budget checks precede the snapshot; ruleset write is followed by readback, and a mismatch stops the operation.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g10 · authorial model

A change creates a unique runtime/deployments/<timestamp-random>--<zone>/ directory containing before.json, write-response.json, and after.json. An existing entry point is updated through one ruleset operation; otherwise, a zone ruleset is created for the phase. Rulesets API

Live state is read again after the write. Remaining drift ends the operation with Post-write verification failed. A successful HTTP response alone does not establish a correct deployment. Case 001 provides no automatic rollback; restoration from a snapshot is a separate reviewed operator action.

Fleet rollout, audit, and export

CommandOperation
php bin/rollout.phpSequential dry-run over the inventory
php bin/rollout.php --applySequential writes; an exception stops subsequent zones
php bin/audit.phpCompare live and desired state across the inventory
php bin/export.phpSave current phase entry points locally
lifecycleFleet control

Zones are checked sequentially; changes are limited by assigned policies, and an error terminates rollout.

  • source: Inventory zones
  • state: Current zone
  • decision: Plan: change needed?
  • human-decision: Has --apply?
  • process: Write and verify
  • process: Next zone
  • boundary: Error: stop
  • inventory produces zone
  • zone references plan
  • plan requires review: Yes
  • plan returns next: NO_CHANGE
  • review validates write: Yes
  • review returns next: Dry-run
  • write produces next: Success
  • write rejects stop: Error
Zones are checked sequentially; changes are limited by assigned policies, and an error terminates rollout.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g11 · authorial model
comparisonDetecting drift

Live state and compiled desired state meet in a comparison returning either agreement or divergence.

  • source: Live state
  • source: Compiled desired state
  • process: Comparison
  • state: Match
  • boundary: DRIFT: review
  • live references compare
  • desired requires compare
  • compare validates ok
  • compare rejects drift
Live state and compiled desired state meet in a comparison returning either agreement or divergence.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g12 · authorial model

Audit reports drift and retains the read-only boundary. Snapshots, reports, and exports stay local. Traversing the entire inventory is also useful for demonstrating that most zones require no changes.

The original pilot’s results

The pilot ran on the earlier private EMBO tools before Case 001 was extracted. The following numbers come from the author’s pilot account; they are not results of a new production run of the standalone code.

StageCustom Rule count
Before: baseline Block, Asia Challenge, Meta-WebIndexer hotfix3
Remove / keep / add2 / 1 / 1
After moving the hotfix’s function into compiled policy2 of the 5 available in the pilot
Smoke requestRecorded HTTP status
Normal /200
Meta /200
Normal /cart/200
Meta /cart/403
Googlebot /cart/403
Meta /checkout/403
Meta /my-account/403
Meta add-to-cart403
Meta remove_item403
Meta wc-ajax403

The author’s smoke summary was 10 PASS, 0 FAIL. Meta and Googlebot name test-request profiles; a substituted User-Agent alone does not test a real verified identity. The homepage’s 200 records access in that test rather than a guarantee for every future request.

Fleet check after the pilotAuthor-reported result
Audit22 OK, 0 DRIFT, 0 ERROR
Diff22 OK, 0 CHANGES, 0 OVER LIMIT, 0 ERROR
Fleet dry-run22 NO_CHANGE

According to the pilot account, the commerce change was assigned to one zone; the other 21 retained their previous expected configuration. These aggregate results express the intended acceptance criterion. Independent reproduction requires your own configuration, a reviewed plan, and an edge smoke.

From pilot to portable pipeline

Earlier tooling functionCase 001 boundary
policy-compiler.phpcompile.php
diff.phpplan.php
deploy.phpapply.php --apply
audit.phpaudit.php
complete-export.phpexport.php
rollout.phprollout.php, separate --apply
validate-expression.php, crawler-surface-smoke.phpSeparate deployment checks; not ported into the current Case 001
flowPortable pipeline

Local configuration and fixtures lead to compile, plan, dry-run, operator review, apply, readback, and audit; site-specific edge smoke remains a separate deployment check.

  • source: Local paths and credentials
  • process: Fixtures
  • process: Compile
  • process: Plan / diff
  • process: Fleet dry-run
  • human-decision: Operator review
  • process: Apply / snapshot / readback
  • process: Separate edge smoke
  • process: Audit / export
  • setup derives fixtures
  • fixtures derives compile
  • compile derives plan
  • plan derives dry
  • dry derives review
  • review derives apply
  • apply derives smoke
  • smoke derives audit
Local configuration and fixtures lead to compile, plan, dry-run, operator review, apply, readback, and audit; site-specific edge smoke remains a separate deployment check.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g13 · authorial model

The operational discipline follows compile → diff/plan → dry-run → apply → smoke → audit. Different commands test different claims: compilation checks the model, plan identifies divergence from live state, smoke tests request behaviour, and audit checks the expected state after a change. Readback does not replace edge smoke.

Telemetry and an analytical LLM layer

Crawler telemetry can report a new traffic class across several hostnames or a zone’s departure from its usual profile. That observation becomes a proposal for a policy revision. Authority to perform a security write remains a separate operator responsibility.

flowFrom telemetry to authorised change

Telemetry opens a proposal; tests, plan, and human review precede explicit apply and verification.

  • source: Telemetry / LLM observation
  • artifact: Policy proposal
  • process: Compile and tests
  • artifact: Plan / diff
  • human-decision: Human review
  • process: Explicit apply
  • process: Verification
  • observe derives proposal
  • proposal derives compile
  • compile derives plan
  • plan derives human
  • human derives apply
  • apply derives verify
Telemetry opens a proposal; tests, plan, and human review precede explicit apply and verification.IRON CREED semantic-core · asset.cloudflare-001-crawler-surface-policy.g14 · authorial model

The analytical layer receives no write token. This allows observation to be automated while changes to site availability remain explicit, reviewable actions.

Boundaries and result

The case addresses a bounded task: express crawler access through identity and surface, assign the policy to the intended zone, and check the rest of the inventory. Faceted navigation, filters, and search require separate decisions; a commerce profile is assigned only after reviewing the actual site.

The result is a shared policy-management system with different effective states for individual zones. The repository separates a reusable mechanism from client data: demonstration domains use domain-N.example, private overlays and tokens stay local, and writes follow a separate controlled path.

Case author and source of the original production account: Sam Starling. Translation, editorial adaptation, questions, public-documentation verification, and standalone-code preparation were assisted by AI. Platform sources were checked on 6 September 2026; preparing this article involved no repeat deployment to the client’s Cloudflare account.