{"slug": "building-a-read-only-cloudflare-worker-ai-security-console", "title": "Building a Read-Only Cloudflare Worker AI Security Console", "summary": "A developer built a read-only Cloudflare Worker AI security console that uses Workers AI to summarize security data without granting the model write access. The console provides workflows for natural language queries, approved query catalogues, security digests, and Ray ID investigations, with Cloudflare Access protecting the interface.", "body_md": "Security teams already have WAF events, bot signals, access logs, and SIEM pipelines. The problem is not always data collection. The problem is turning that data into a fast, readable operational view without giving a model unsafe authority.\n\nThis implementation uses a Cloudflare Worker as the control layer and Workers AI as the summarization layer. The Worker is deliberately read-only.\n\nIt supports four workflows:\n\nFor staging examples, I will use `example.com.dev`\n\n. Do not treat that as a real environment.\n\nThe model should not be the administrator.\n\nThe Worker owns:\n\nWorkers AI owns:\n\nThat split matters. If the model is allowed to create arbitrary queries or perform Cloudflare write actions, the tool becomes much harder to govern.\n\n``` php\nflowchart TD\n    A[Security analyst] --> B[Cloudflare Access]\n    B --> C[sentinel-cf Worker]\n    C --> D[Scope and input validation]\n    D --> E[Cloudflare Analytics GraphQL]\n    D --> F[Workers AI via AI Gateway]\n    E --> G[Normalized metrics]\n    F --> H[Summary or explanation]\n    G --> I[HTML report]\n    H --> I\n    C --> J[Workers KV latest digest]\n```\n\n| Workflow | Endpoint | Output |\n|---|---|---|\n| Main menu | `/` |\nHTML |\n| Natural language query | `/query` |\nHTML table/cards with raw JSON toggle |\n| Approved query catalogue | `/allowed-queries` |\nHTML list with Copy and Use actions |\n| Security digest | `/digest?range=7d` |\nHTML report |\n| Latest scheduled digest | `/digest/latest` |\nHTML report from KV |\n| Ray ID investigation | `/ray` |\nHTML investigation report |\n| Health check | `/healthz` |\nJSON |\n\nThis implementation should not:\n\nThe right production pattern is: AI recommends, humans approve, Terraform or approved change control applies.\n\nYou need:\n\n`AI`\n\n.Cloudflare documents Worker deployment through Terraform using `cloudflare_worker`\n\n, `cloudflare_worker_version`\n\n, and `cloudflare_workers_deployment`\n\n. Worker version modules should use `content_file`\n\nwhere practical to avoid storing large Worker code directly in Terraform state. See [Cloudflare Workers IaC](https://developers.cloudflare.com/workers/platform/infrastructure-as-code/).\n\nUse these Worker bindings and variables:\n\n| Type | Name | Example |\n|---|---|---|\n| Workers AI binding | `AI` |\nWorkers AI Catalog |\n| KV binding | `DIGEST_KV` |\n`sentinel-cf-uat-digests` |\n| Secret | `CF_ANALYTICS_TOKEN` |\nRedacted |\n| Plain variable | `APP_HOSTNAME` |\n`sentinel-cf.example.workers.dev` |\n| Plain variable | `AI_GATEWAY_ID` |\n`sentinel-cf-gateway` |\n| Plain variable | `ALLOWED_ZONE_TAGS` |\nZone IDs, comma-separated |\n| Plain variable | `ENVIRONMENT` |\n`uat` |\n| Plain variable | `DEFAULT_DIGEST_RANGE` |\n`7d` |\n| Plain variable | `DIGEST_TITLE` |\n`SENTINEL-CF Security Posture Digest` |\n\n`ALLOWED_ZONE_TAGS`\n\nmust contain Cloudflare Zone IDs, not domain names such as `example.com.dev`\n\n.\n\nFor UAT, I prefer building the first version in the Cloudflare console. The console path makes it easier to prove each moving part before Terraform becomes the source of truth.\n\nThe practical order is:\n\nIn Cloudflare, create a dedicated API token for this Worker:\n\n``` php\nManage account -> Account API Tokens -> Create Token -> Create Custom Token\n```\n\nUse the minimum read-only permission required by the Analytics GraphQL API:\n\n```\nPermission group: Analytics\nPermission: Read\nScope: Selected zone only\nZone: example.com.dev\n```\n\nDo not grant WAF edit, DNS edit, Access edit, Rules edit, or account administrator permissions.\n\nStore the value as:\n\n```\nCF_ANALYTICS_TOKEN\n```\n\nYou will add it to the Worker as a secret later. Do not paste it into the Worker JavaScript.\n\nIn Cloudflare:\n\n``` php\nWorkers & Pages -> Create Worker -> Start with Hello World\n```\n\nUse a simple name:\n\n```\nsentinel-cf\n```\n\nDeploy the starter Worker first. This proves the Worker route exists before adding the full security console code.\n\nBefore exposing the security console, put Cloudflare Access in front of it.\n\nIn the Worker creation flow or the Worker Access tab:\n\n```\nProtect with Cloudflare Access: On\nScope: All traffic\nPolicy action: Allow\nPolicy name: sentinel-cf-bootstrap-admin\n```\n\nFor a single UAT administrator, use your own verified identity. For a team, use an Access group or identity provider group instead of adding users one by one.\n\nA production policy should usually require:\n\nOpen the Worker:\n\n``` php\nWorkers & Pages -> sentinel-cf -> Bindings -> Add binding -> Workers AI\n```\n\nSet:\n\n```\nVariable name: AI\n```\n\nWorkers AI bindings allow Worker code to invoke models through `env.AI.run(...)`\n\n. See [Workers AI bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/).\n\nCreate a KV namespace for the latest scheduled digest:\n\n``` php\nWorkers & Pages -> KV -> Create namespace\n```\n\nUse:\n\n```\nsentinel-cf-uat-digests\n```\n\nThen bind it to the Worker:\n\n``` php\nWorkers & Pages -> sentinel-cf -> Bindings -> Add binding -> KV namespace\n```\n\nSet:\n\n```\nVariable name: DIGEST_KV\nKV namespace: sentinel-cf-uat-digests\n```\n\nKV is used only for storing the latest generated digest. It is not used for secrets.\n\nCreate an AI Gateway for observability and control:\n\n``` php\nAI -> AI Gateway -> Create gateway\n```\n\nUse:\n\n```\nGateway ID: sentinel-cf-gateway\nCollect logs: On, if approved by your security policy\nCache responses: Off for security analytics\nRate limit requests: On for production\nSpend limits: On for production\nAuthenticated Gateway: On where available\n```\n\nSecurity analytics can contain sensitive paths, rule names, source geography, and investigation context. Treat AI Gateway logs as security logs and restrict who can read them.\n\nOpen:\n\n``` php\nWorkers & Pages -> sentinel-cf -> Settings -> Variables and Secrets\n```\n\nAdd this secret:\n\n```\nType: Secret\nName: CF_ANALYTICS_TOKEN\nValue: <the read-only analytics token>\n```\n\nAdd these plain variables:\n\n```\nAPP_HOSTNAME=sentinel-cf.example.workers.dev\nAI_GATEWAY_ID=sentinel-cf-gateway\nALLOWED_ZONE_TAGS=<cloudflare-zone-id>\nENVIRONMENT=uat\nDEFAULT_DIGEST_RANGE=7d\nDIGEST_TITLE=SENTINEL-CF Security Posture Digest\n```\n\n`ALLOWED_ZONE_TAGS`\n\nmust be Cloudflare Zone IDs, not domain names. If you allow more than one zone, use a comma-separated list:\n\n```\nALLOWED_ZONE_TAGS=<zone-id-1>,<zone-id-2>\n```\n\nOpen:\n\n``` php\nWorkers & Pages -> sentinel-cf -> Edit code\n```\n\nReplace the starter Worker code with the `sentinel-cf-worker.js`\n\nimplementation and deploy it.\n\nThe deployed Worker should render HTML by default. JSON should be available only where intentionally exposed, such as `/healthz`\n\nor the `Show raw JSON`\n\nsection in query results.\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/healthz\n```\n\nExpected:\n\n```\n{\n  \"status\": \"ok\",\n  \"mode\": \"read_only\",\n  \"features\": [\"nlq\", \"allowed_query_catalog\", \"digest\", \"ray_id_investigator\"]\n}\n```\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/allowed-queries\n```\n\nThe page should show approved analyst questions with:\n\n`Copy`\n\n.`Use`\n\n.`Use`\n\nopens `/query`\n\nwith the selected question and default window pre-filled. The Worker still enforces fixed read-only intents and zone allowlisting.\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/query\n```\n\nTry approved defensive questions such as:\n\n```\nShow me top source countries today\nShow potential SQL injection events\nShow blocked WAF events in the last 24 hours\nShow top targeted URLs this week\nShow noisy WAF rules in the last 7 days\n```\n\nThe Security NLQ page should return readable cards and tables, not raw JSON by default. Raw JSON remains available behind `Show raw JSON`\n\nfor validation.\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/digest?range=7d\n```\n\nYou should see:\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/ray\n```\n\nEnter a Ray ID from Cloudflare Security Events or response headers.\n\nCloudflare Ray IDs are useful for correlating a request across Security Events, Log Explorer, and server logs, but Cloudflare notes they are not guaranteed unique in all situations. See [Cloudflare Ray ID](https://developers.cloudflare.com/fundamentals/reference/cloudflare-ray-id/).\n\nAfter manual digest testing works, add a weekly Cron Trigger:\n\n``` php\nWorkers & Pages -> sentinel-cf -> Settings -> Trigger events -> Cron triggers -> Add\n```\n\nUse a weekly UTC schedule:\n\n```\n0 1 * * 1\n```\n\nCloudflare Cron Triggers run on UTC time and can take several minutes to propagate. See [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/).\n\nWhen the cron runs, the Worker should generate the digest and store the latest copy in:\n\n```\nDIGEST_KV\n```\n\nOpen:\n\n```\nhttps://sentinel-cf.example.workers.dev/digest/latest\n```\n\nto confirm the stored digest renders.\n\nAfter the console deployment is working, use Terraform to make the setup repeatable for UAT and production. The Terraform should represent the proven console configuration rather than introducing a separate design.\n\nA clean Terraform handoff should include:\n\n```\nmain.tf\nvariables.tf\noutputs.tf\nterraform.tfvars.example\nsentinel-cf-worker.js\ncron-trigger.tf.example\nREADME.md\n```\n\nKeep the Worker JavaScript in `sentinel-cf-worker.js`\n\nand reference it from Terraform. This keeps the code reviewable and avoids burying a large Worker body inside Terraform.\n\nThe important resources are:\n\n```\nresource \"cloudflare_worker\" \"sentinel_cf\" {\n  account_id = var.cloudflare_account_id\n  name       = \"sentinel-cf\"\n}\n\nresource \"cloudflare_workers_kv_namespace\" \"sentinel_cf_digests\" {\n  account_id = var.cloudflare_account_id\n  title      = \"sentinel-cf-${var.environment}-digests\"\n\n  lifecycle {\n    prevent_destroy = true\n  }\n}\n```\n\nThe Worker version should bind Workers AI, KV, plain variables, and the read-only secret:\n\n```\nbindings = [\n  {\n    type = \"ai\"\n    name = \"AI\"\n  },\n  {\n    type         = \"kv_namespace\"\n    name         = \"DIGEST_KV\"\n    namespace_id = cloudflare_workers_kv_namespace.sentinel_cf_digests.id\n  },\n  {\n    type = \"plain_text\"\n    name = \"APP_HOSTNAME\"\n    text = var.app_hostname\n  },\n  {\n    type = \"plain_text\"\n    name = \"AI_GATEWAY_ID\"\n    text = var.ai_gateway_id\n  },\n  {\n    type = \"plain_text\"\n    name = \"ALLOWED_ZONE_TAGS\"\n    text = join(\",\", var.allowed_zone_tags)\n  },\n  {\n    type = \"plain_text\"\n    name = \"ENVIRONMENT\"\n    text = var.environment\n  },\n  {\n    type = \"plain_text\"\n    name = \"DEFAULT_DIGEST_RANGE\"\n    text = var.default_digest_range\n  },\n  {\n    type = \"plain_text\"\n    name = \"DIGEST_TITLE\"\n    text = var.digest_title\n  },\n  {\n    type = \"secret_text\"\n    name = \"CF_ANALYTICS_TOKEN\"\n    text = var.cf_analytics_token\n  }\n]\n```\n\nExpose the important URLs as outputs:\n\n```\noutput \"worker_url\" {\n  value = \"https://${var.app_hostname}\"\n}\n\noutput \"allowed_queries_url\" {\n  value = \"https://${var.app_hostname}/allowed-queries\"\n}\n\noutput \"digest_url\" {\n  value = \"https://${var.app_hostname}/digest?range=${var.default_digest_range}\"\n}\n\noutput \"ray_investigator_url\" {\n  value = \"https://${var.app_hostname}/ray\"\n}\n```\n\nRun:\n\n```\nterraform init\nterraform fmt -recursive\nterraform validate\nterraform plan\n```\n\nIf the Worker or Access app already exists from the dashboard, import existing resources before apply. Do not let Terraform destroy and recreate Access controls without explicit approval.\n\nThe recommended production migration is:\n\n`terraform plan`\n\nand review the proposed changes.| Test | Expected result |\n|---|---|\n`/healthz` |\nWorker healthy and read-only. |\n`/allowed-queries` |\nApproved query list renders; Copy and Use actions work. |\n`/query` |\nHTML cards/tables render; events show Bangkok-local time and selected window labels. |\n`/digest?range=7d` |\nHTML digest renders; noisy rules include rule name and rule ID when available. |\n`/digest/latest` |\nLatest scheduled digest renders from KV after cron has run. |\n`/ray` |\nRay investigation form loads. |\n| Invalid zone | Request is rejected. |\n| Write-style prompt | No change is performed. |\n| Unauthenticated request | Blocked by Cloudflare Access. |\n\nBefore production:\n\nThis is a useful pattern because it gives analysts a faster way to understand Cloudflare security activity without handing automation unsafe authority.\n\nThe Worker is the guardrail. AI is the analyst assistant. Terraform is the control plane. That division is what makes the design operationally credible.", "url": "https://wpnews.pro/news/building-a-read-only-cloudflare-worker-ai-security-console", "canonical_source": "https://dev.to/mike_anderson_d01f52129fb/building-a-read-only-cloudflare-worker-ai-security-console-4ica", "published_at": "2026-08-25 10:11:12+00:00", "updated_at": "2026-08-25 10:44:10.879624+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "ai-safety", "developer-tools"], "entities": ["Cloudflare", "Workers AI", "Cloudflare Access", "Cloudflare Analytics GraphQL", "Terraform", "Workers KV"], "alternates": {"html": "https://wpnews.pro/news/building-a-read-only-cloudflare-worker-ai-security-console", "markdown": "https://wpnews.pro/news/building-a-read-only-cloudflare-worker-ai-security-console.md", "text": "https://wpnews.pro/news/building-a-read-only-cloudflare-worker-ai-security-console.txt", "jsonld": "https://wpnews.pro/news/building-a-read-only-cloudflare-worker-ai-security-console.jsonld"}}