Schedule icon
List icon
Script icon
If icon
Switch icon
Loop icon
Delete icon
Log icon
SlackIncomingWebhook icon

Cloudflare WAF Overly Broad Allowlist Gate

Audit Cloudflare WAF access rules for country- or ASN-scoped whitelist rules, alert on breach, and optionally remove them.

Categories
CloudInfrastructure

A Cloudflare WAF whitelist rule scoped to a specific IP is a narrow, reviewable exception. The same rule scoped to an entire country or Autonomous System Number is a different thing entirely - it waves an unknown number of IP addresses straight past every other WAF control for that zone, and it is exactly the kind of rule that gets created during an incident ("just allow all of AS12345 through, we'll narrow it later") and then never gets narrowed. This blueprint lists a zone's WAF IP access rules, flags any whitelist rule scoped this broadly, alerts Slack on breach, and can delete the offending rules - leaving every narrowly-scoped allow rule and every block or challenge rule untouched.

How it works

  1. periodic_allowlist_check (io.kestra.plugin.core.trigger.Schedule) runs every 6 hours, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. audit_waf_access_rules (io.kestra.plugin.cloudflare.waf.accessrules.List) lists the zone's IP access rules, each carrying id, mode, and configuration.{target,value}.
  3. evaluate_allowlist_risk (io.kestra.plugin.scripts.python.Script) collects every rule where mode == "whitelist" and configuration.target is "country" or "asn", emitting broad_count and broad_rules through Kestra.outputs.
  4. evaluate_allowlist_gate (io.kestra.plugin.core.flow.If) flags a breach whenever broad_count > max_broad_allow_rules.
    • then (breach): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, remove_broad_allow_rules (io.kestra.plugin.core.flow.Loop) runs delete_broad_allow_rule (io.kestra.plugin.cloudflare.waf.accessrules.Delete) for each flagged rule. alert_allowlist_risk always fires to Slack regardless of which path ran.
    • else (compliant): log_allowlist_healthy records a clean check.
  5. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  6. The errors block alerts Slack separately if the flow itself fails - an invalid token or unreachable zone must not read as "no breach".

What you get

  • A concrete, named list of overly broad WAF allow rules, not a generic "review your firewall rules" reminder.
  • Remediation that only ever deletes the specific country/ASN-scoped whitelist rules flagged; every narrowly-scoped allow rule and every block/challenge rule is left exactly as it was.
  • Two independent gates (auto_remediate, dry_run) between "breach detected" and "rule actually deleted".
  • A final status log every run, breach or not, so the execution history doubles as a WAF-posture trend for the zone.

Who it's for

  • Platform and security teams running Cloudflare who need a standing check against incident-response allow rules that were never narrowed or removed.
  • SREs who want this as a recurring hygiene pass after every incident where a temporary broad allowlist was created under pressure.
  • Anyone evaluating the Cloudflare plugin who wants a worked example of waf.accessrules.List beyond the temporary-rule pattern already shown in this repo's cloudflare-canary-deploy.yaml.

Why orchestrate this with Kestra

Cloudflare's own dashboard shows the current rule list, but it has no scheduled audit, no threshold logic, and no notification path for "this allow rule is scoped too broadly." Kestra supplies the schedule, the risk classification as a first-class branch, two independent authorization gates before anything is deleted, and an execution history that shows exactly when an overly broad rule appeared and whether it was removed.

Prerequisites

  • A Cloudflare zone and an API token with rights to list and, if remediation runs, delete IP access rules for that zone.
  • A Slack incoming webhook for alerts.

Local testing: this plugin calls Cloudflare's own hosted API directly (baseUrl defaults to https://api.cloudflare.com/client/v4); there is no self-hosted Cloudflare emulator to run in Docker. Validate this flow against a real, low-traffic test zone - for example by first running cloudflare-canary-deploy.yaml's allow_canary_source task (already in this repo) to create a throwaway country-scoped rule for a test zone, confirming this blueprint flags it, then letting this blueprint's own remediation remove it.

Secrets

  • CLOUDFLARE_API_TOKEN: Cloudflare API token, matching this plugin's own documented pattern (see cloudflare-canary-deploy.yaml).
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_allowlist_risk and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_CLOUDFLARE_API_TOKEN=$(echo -n '<your-cloudflare-token>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • zone_id (STRING, example default 023e105f4ecef8ad9ca31a8372d0c353): Cloudflare zone audited; replace it with your zone ID.
  • max_broad_allow_rules (INT, default 0): breach threshold; 0 means any country/ASN-scoped whitelist rule at all breaches.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for deletion to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for the rule to actually be deleted.

Outputs

  • outputs.audit_waf_access_rules.rules: the raw access-rule listing (id, mode, configuration.target, configuration.value) for every run.
  • outputs.evaluate_allowlist_risk.vars.broad_count / .broad_rules: the computed policy result for every run.

Quick start

  1. Create a test whitelist rule scoped to country or asn on a non-production zone (for example via cloudflare-canary-deploy.yaml's allow_canary_source task, or directly in the Cloudflare dashboard).
  2. Add the CLOUDFLARE_API_TOKEN and SLACK_WEBHOOK_URL secrets.
  3. Run the flow manually with the defaults (auto_remediate: "false") and confirm alert_allowlist_risk fires naming the expected rule, and that log_remediation_disabled logs without deleting anything.
  4. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the deletion without running it.
  5. Run once with auto_remediate: "true" and dry_run: "false" and confirm via the Cloudflare dashboard or waf.accessrules.List that the flagged rule is gone.
  6. Enable periodic_allowlist_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Page through results beyond the first page - this audit currently only sees what List's documented first-page pagination returns; see Pitfalls.
  • Loop over several zone_id values with io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to sweep every zone in an account in one run.
  • Extend evaluate_allowlist_risk to also flag ip_range targets wider than a configurable CIDR prefix length, once you need finer-grained scope than the binary country/ASN check this blueprint uses.
  • Route the remediation branch through a Pause approval task instead of (or alongside) the static auto_remediate input, for a human-reviewed deletion on production zones.

Pitfalls

  • List's documented output has no notes, created_on, or scope field. Verified from the plugin's own output schema, which only documents id, mode, and configuration.{target,value}. This is why the audit is scoped to rule mode and target type rather than rule age or an expiry tag - those fields are not confirmed to exist in this task's output.
  • List only returns the first page of results. Verified from the plugin's own description: "Lists IP access rules for a zone using Cloudflare default pagination (first page only)." A zone with more access rules than fit on one page will be under-audited by this blueprint as written; see How to extend.
  • Deleting an access rule is not reversible by this flow. Unlike this session's other remediations (which only move, flag, or restrict), waf.accessrules.Delete permanently removes the rule; if a deleted country/ASN-scoped rule was actually intentional, it must be recreated by hand. Both auto_remediate and dry_run exist specifically to make this an explicit, opt-in action.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for the delete Loop to run; setting only one leaves the other still blocking.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.