Cloudflare¶
The Cloudflare LimaCharlie Extension exposes the incident-response and investigation surface of a Cloudflare account/zone (the Cloudflare v4 API) to D&R rules and AI agents. It enables automated edge and Zero Trust containment directly from detections — block a malicious source at the WAF, revoke a Zero Trust Access user's sessions, push IOCs onto a Gateway block list, purge cache, or fix a hijacked DNS record — without bouncing through a separate SOAR.
The extension provides two layers:
- Typed actions for the common containment, triage, and investigation workflows, with friendly parameter names and built-in safety rails.
- A generic
api_callpassthrough for any Cloudflare v4 endpoint not covered by a typed action, including non-enveloped surfaces like the GraphQL Analytics API.
Authentication is a scoped API Token (Bearer) or the legacy Global API Key — both static, no user interaction.
Setup¶
1. Create an API Token¶
In the Cloudflare dashboard (My Profile → API Tokens, or Account Home → Manage Account → API Tokens), create a token and grant only the permission groups for the actions you will use, scoped to the specific account and/or zone rather than "all accounts / all zones". The least-privilege set per capability:
| Capability | Permission group (Read/Edit) | Scope |
|---|---|---|
| IP Access Rules (account) | Account Firewall Access Rules — Edit | Account |
| IP Access Rules (zone) | Firewall Services — Edit | Zone |
| WAF custom rules / rulesets | Account WAF / Zone WAF + Rulesets — Edit | Account / Zone |
| Zero Trust Access (users, sessions, policies) | Access: Apps and Policies + Access: Organizations, Identity Providers, and Groups — Edit | Account |
| Gateway lists & rules | Zero Trust — Edit | Account |
| DNS read / edit | DNS — Read / Edit | Zone |
| Cache purge | Cache Purge — Purge | Zone |
| Audit log | Logs — Read | Account |
| Firewall events (GraphQL) | Account Analytics — Read | Account |
| Account members | Account Membership — Edit | Account |
Only add what you will use — a valid token missing a permission group returns 403 on that endpoint (that is a scope problem, not a bad token). Resolve current permission-group names/IDs at setup via GET /accounts/{account_id}/tokens/permission_groups.
A legacy Global API Key works too (your account email plus the key), but it has full account access and cannot be scoped — prefer a token.
2. Subscribe to the extension¶
Subscribe to ext-cloudflare from the LimaCharlie Marketplace (Extensions → Add-Ons).
3. Store the secret¶
In Secrets Manager, create a new secret (for example cloudflare-api-token) and paste the API token (or Global API Key) as its value.
4. Configure the extension¶
In Extensions → ext-cloudflare → Configuration, fill in:
| Field | Required | Value |
|---|---|---|
api_token |
one auth mode | Reference to the API token secret, e.g. hive://secret/cloudflare-api-token. |
email |
legacy only | Account email for legacy Global API Key auth (paired with api_key). |
api_key |
legacy only | Reference to the Global API Key secret (paired with email). |
account_id |
no | Default account id for account-scoped actions. Any action can override it. |
zone_id |
no | Default zone id for zone-scoped actions. Any action can override it. |
Provide either api_token or email + api_key, not both.
Scoping: account vs zone¶
Cloudflare resources are either account-scoped or zone-scoped. Set default account_id / zone_id in the config; any action can override them per-request.
- Account-scoped: Zero Trust Access, Gateway, account members, the audit log.
- Zone-scoped: DNS records, cache purge.
- Either: IP Access Rules and WAF custom rules accept
account_idorzone_id— account scope applies across all the account's zones, zone scope to one zone. Pass exactly one; passing both is rejected as ambiguous, and with neither set the config defaults are used, preferring the zone (the narrower blast radius).
Actions¶
Every action that targets an entity requires an explicit selector (an ip / asn / country, a rule_id, an email, a list_id + values, a dns_record_id, a member_id, …) — the extension refuses to run without one, preventing an accidental account-wide response. purge_cache additionally refuses unless exactly one purge mode is chosen.
Typed list actions return {data: [...], pagination: {...}}, where pagination carries Cloudflare's result_info verbatim — offset fields (page, per_page, total_pages, …) or a cursor. Page by incrementing page until page == total_pages, or by passing the cursor back. per_page is clamped to 100.
Generic¶
api_call¶
Generic passthrough to any Cloudflare v4 endpoint.
| Field | Type | Notes |
|---|---|---|
method |
enum | GET (default), POST, PUT, PATCH, DELETE. |
path |
string | Required. Path relative to https://api.cloudflare.com/client/v4 (e.g. zones/{zone_id}/dns_records, or graphql for the GraphQL Analytics API) or a full URL. |
query |
object | Query-string parameters. |
headers |
object | Extra request headers. |
body |
object | JSON body for POST/PUT/PATCH. |
Returns the whole response body verbatim — the {success, errors, result, result_info} envelope, or the raw shape for non-enveloped endpoints like GraphQL.
Investigation (reads)¶
| Action | Parameters | What it does |
|---|---|---|
verify_token |
— | Verify the configured API token (health check); returns the token status. |
list_accounts |
name, page, per_page, extra_query |
List accounts the credential can see — find an account_id. |
list_zones |
name, account_id, status, page, per_page, extra_query |
List/search zones — find a zone_id. |
list_ip_access_rules |
account_id/zone_id, target, value, mode, page, per_page |
List IP Access Rules; use a returned rule id with remove_ip_access_rule. |
list_dns_records |
zone_id, type, name, content, page, per_page |
List a zone's DNS records; use a returned id with edit_dns_record / delete_dns_record. |
list_access_users |
account_id, email, name, search, page, per_page |
List Zero Trust Access users. |
get_access_user_activity |
account_id, user_id, kind |
An Access user's active_sessions, last_seen_identity, or failed_logins. |
list_gateway_lists |
account_id, page, per_page |
List Zero Trust Gateway lists — find a list_id. |
list_members |
account_id, status, page, per_page |
List account members — use a returned membership id (result[].id, not user.id) with remove_member. |
get_audit_logs |
account_id, since, before, action_type, actor_email, limit, cursor, direction |
Query the account audit log (v2, cursor-paginated). The v2 API requires a since+before window; if omitted it defaults to the last 7 days. Pass the same explicit window across pages for stable pagination. |
search_firewall_events |
zone_id, since, until, client_ip, action, limit |
Search a zone's WAF/firewall events via the GraphQL Analytics API (firewallEventsAdaptive). Counts are sampled; the window is plan-dependent (~31 days). |
get_waf_custom_ruleset |
account_id/zone_id |
Fetch the http_request_firewall_custom phase ruleset — its id is the ruleset_id for add_waf_custom_rule. |
Edge / WAF containment¶
| Action | Parameters | What it does |
|---|---|---|
block_ip |
ip, account_id/zone_id, mode, notes |
Block a single IP (v4/v6) via an IP Access Rule. |
block_ip_range |
range, account_id/zone_id, mode, notes |
Block a CIDR range (IPv4 /16 or /24; IPv6 /32, /48, /64). |
block_asn |
asn, account_id/zone_id, mode, notes |
Block an ASN (bare AS number, no AS prefix). |
block_country |
country, account_id/zone_id, mode, notes |
Block a country (ISO-3166-1 alpha-2). |
remove_ip_access_rule |
rule_id, account_id/zone_id |
Delete an IP Access Rule (undo a block) at the scope it was created. |
add_waf_custom_rule |
expression, account_id/zone_id, action, description, ruleset_id |
Add a WAF custom rule (wirefilter expression, e.g. (ip.src eq 198.51.100.4)); resolves the custom-phase ruleset for you if ruleset_id is omitted. |
mode on the block actions defaults to block; override with challenge, managed_challenge, js_challenge, or whitelist (an allow-list entry). An account-scoped rule applies across all the account's zones.
Zero Trust containment¶
| Action | Parameters | What it does |
|---|---|---|
revoke_access_user |
email, account_id, revoke_devices (default true), warp_session_reauth |
Revoke a Cloudflare Access user's sessions (by email); revoke_devices also drops device/WARP sessions. |
gateway_add_to_blocklist |
list_id, values, account_id |
Append IOCs (domains, IPs, URLs, … per the list type) to a Zero Trust Gateway list. |
gateway_remove_from_blocklist |
list_id, values, account_id |
Remove IOCs from a Gateway list (undo). |
revoke_access_user propagates in ~1 minute and does not disable the upstream IdP identity — pair it with an IdP-side disable or a deny Access policy (via api_call) for a durable block. Wire a Gateway list into a Gateway block rule for it to enforce.
DNS / cache response¶
| Action | Parameters | What it does |
|---|---|---|
edit_dns_record |
dns_record_id, zone_id, content, name, type, ttl, proxied, comment |
Partially update a DNS record (only set fields change) — e.g. re-point a hijacked record to a sinkhole. |
delete_dns_record |
dns_record_id, zone_id |
Delete a DNS record — e.g. remove an attacker-created record. |
purge_cache |
zone_id, purge_everything, files, hosts, tags, prefixes |
Purge cache. Choose exactly one mode: purge_everything=true, or one of files / hosts / tags / prefixes (hosts/tags/prefixes are Enterprise-only). |
Account containment¶
| Action | Parameters | What it does |
|---|---|---|
remove_member |
member_id, account_id |
Remove a member from the account. Use the membership id (result[].id from list_members, not user.id). To downgrade instead of remove, api_call a PUT with a read-only role. |
Detection & Response¶
Example response action that blocks the source IP named in a detection at the Cloudflare edge:
- action: extension request
extension action: block_ip
extension name: ext-cloudflare
extension request:
ip: '{{ .event/SOURCE_IP }}'
zone_id: '{{ "<your-zone-id>" }}'
notes: '{{ "Blocked by LimaCharlie D&R rule" }}'
Wrap literal strings in
{{ "..." }}. Values underextension requestare evaluated as templates. A bare string without{{ }}is interpreted as a gjson path against the event and, if it doesn't resolve, the key is silently dropped from the payload.
extension request actions are fire-and-forget — the rule engine does not surface the response back into the rule's evaluation context. Workflows that chain (look up the zone, block the IP, then confirm) belong in a Playbook or an AI agent, which can hold ids between calls.
Notes¶
success: falseis an error. Cloudflare frequently answers200withsuccess: falseon a logical error; the extension treats that as a failure and surfaces the Cloudflare error code/message, so a typed action never reports a no-op as success.- IP Access Rule scope matters. An account-scoped rule applies across all the account's zones; a zone-scoped rule to one zone.
remove_ip_access_rulemust delete at the same scope the rule was created. revoke_access_useris not a durable block. It kills current sessions but does not disable the IdP identity; pair with an IdP disable or adenypolicy.- Purge is explicit.
purge_cacherequires exactly one mode;purge_everythingmust be set totruedeliberately.tags/hosts/prefixesare Enterprise-only. - User API tokens can't be revoked cross-member. Cloudflare's token-revocation endpoint is user-scoped; to contain another member,
remove_member(or downgrade them viaapi_call). - Secret rotation recovers automatically. A
401(or a403/400carrying Cloudflare's authentication error code10000) evicts the cached client and re-reads the secret from Secrets Manager on the next call; a403permission gap is not treated as an auth failure. Error messages are formattedcloudflare api error <status> on <method> <path>: <code> <message>(cloudflare auth error …for auth failures), with query strings redacted. - Unsubscribing from the extension preserves its saved configuration; re-subscribing restores it without reconfiguration.