Skip to content

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 in frontend/e2e/cloudflare_mock.py.

  • 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 8080 on a host the Warden backend can reach. A mock on a private address also needs ALLOW_INTERNAL_WEBHOOK_IPS=true on that Warden, since the SSRF guard refuses private targets.

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

Settings → Integration Packs → cloudflare-waf → Install. Accept the preview. custom.cloudflare_waf_block, _unblock and _status are now registered.

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"
}
]
}

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-walkthrough
name: Cloudflare TTL walkthrough
version: 1
enabled: true
trigger:
type: manual
inputs:
- { 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: fail

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.7 with the walkthrough note. On the mock, GET <mock>/__mock/rules lists it under created and active.

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 names custom.cloudflare_waf_unblock with 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.

Post the run id, effect id, rule id and the active and expired timestamps on the issue that asked for the proof.

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:

Terminal window
CLOUDFLARE_MOCK_URL=http://cloudflare-mock:8080 pnpm e2e golden-path-cloudflare

It 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.