Cloudflare TTL walkthrough: block, expire, unblock
Source of truth:
warden/docs/cloudflare-ttl-walkthrough.md(synced at build time — edit the original, not this copy). This walkthrough proves the response spine end to end against Cloudflare: an approved block of one IP becomes an account IP access rule, the TTL runs out, and Warden deletes the rule through the same adapter and records the reversal on the case. Run it against a real Cloudflare account, or against the mock infrontend/e2e/cloudflare_mock.py.
What you need
Section titled “What you need”- A Warden admin session.
- For the live run: a Cloudflare account id and an API token with
Account Firewall Access Rules: Edit on that account. The target below is
203.0.113.7(TEST-NET-3), so blocking it can’t affect real traffic. - For the mock run:
python3 frontend/e2e/cloudflare_mock.py --port 8080on a host the Warden backend can reach. A mock on a private address also needsALLOW_INTERNAL_WEBHOOK_IPS=trueon that Warden, since the SSRF guard refuses private targets.
1. Connection
Section titled “1. Connection”Settings → Connections → New connection:
| Field | Value |
|---|---|
| Kind | http |
| Name | cloudflare |
| Base URL | https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID> (or the mock URL) |
| Authentication | Static header, header name Authorization, value prefix Bearer |
| Secret | the API token |
2. Install the pack
Section titled “2. Install the pack”Settings → Integration Packs → cloudflare-waf → Install. Accept the preview.
custom.cloudflare_waf_block, _unblock and _status are now registered.
3. Bind the adapter
Section titled “3. Bind the adapter”PUT /api/response-policy as an admin. The TTL floor has to allow the short
TTL used below:
{ "protected_targets": [{"type": "ip_range", "value": "10.0.0.0/8"}], "ttl": {"min_seconds": 60, "max_seconds": 604800}, "automatic_grants": [], "adapters": [ { "effect_type": "block", "target_type": "ip", "adapter_id": "cloudflare-waf", "block_action": "custom.cloudflare_waf_block", "unblock_action": "custom.cloudflare_waf_unblock", "status_action": "custom.cloudflare_waf_status", "connection_name": "cloudflare" } ]}4. A short-TTL runbook
Section titled “4. A short-TTL runbook”The shipped bruteforce-response blocks for 24 hours. For the walkthrough, save
this one (Runbooks → New, or POST /api/runbooks/cloudflare-ttl-walkthrough
with {"yaml_text": ...}):
id: cloudflare-ttl-walkthroughname: Cloudflare TTL walkthroughversion: 1enabled: truetrigger: type: manualinputs: - { name: ip, type: string, required: true }steps: - id: block_source action: response.ip_block approval: { roles: [approver, admin], timeout_h: 1, message: "Block {{ inputs.ip }} for 120s?" } with: ip: "{{ inputs.ip }}" ttl_seconds: 120 reason: "Warden TTL walkthrough {{ case.key }}" on_error: fail5. Block
Section titled “5. Block”Open a case, run cloudflare-ttl-walkthrough with ip = 203.0.113.7, and
approve block_source from a different account than the one that started the
run.
Check:
- The run detail lists one response effect in
active, with the Cloudflare rule id as its external reference. - Cloudflare (Security → WAF → Tools → IP Access Rules) shows a block rule for
203.0.113.7with the walkthrough note. On the mock,GET <mock>/__mock/ruleslists it undercreatedandactive.
6. Expiry and unblock
Section titled “6. Expiry and unblock”Wait out the TTL. The worker’s expiry sweep picks the effect up within a minute
or two of expires_at.
Check:
- The effect moves to
expired, and its reverse attempt namescustom.cloudflare_waf_unblockwith the same rule id. - The case timeline shows the effect’s expiry.
- The rule is gone from Cloudflare. On the mock, its id is under
deleted.
7. Evidence
Section titled “7. Evidence”Post the run id, effect id, rule id and the active and expired timestamps on
the issue that asked for the proof.
Automated version
Section titled “Automated version”frontend/e2e/golden-path-cloudflare.spec.ts runs steps 1 to 6 against the mock
with a 60 second TTL, plus a second test showing that with no adapter bound the
same block ends degraded with integration_not_configured:
CLOUDFLARE_MOCK_URL=http://cloudflare-mock:8080 pnpm e2e golden-path-cloudflareIt needs a running stack, an identity session that starts the run
(VELTRO_E2E_BASE_URL, VELTRO_E2E_STORAGE_STATE) and a second approver’s
session (VELTRO_E2E_APPROVER_STORAGE_STATE), because four-eyes stops the
requester approving their own run. It runs in WSL or on the homelab, not in CI.
To read the block back, run response.ip_status on the same IP while the
block is active (bruteforce-response’s confirm_block step does). cloudflare-waf
reads a rule by id, so the read takes the rule id from that IP’s active block
and reports enforced only while the rule’s mode is block. After the reversal
there’s no active block to take an id from, and the read ends
adapter_input_unsatisfied:rule_id; check the mock’s /__mock/rules then.