WAF Rules skill for Codex
Count first, block later, except narrowly. Codex rolls out WAF protection the cautious way: logging first, managed rules in count or log mode through a business peak, then per-rule, per-path exceptions for confirmed false positives before switching to block. It writes Cloudflare and AWS WAF rules as Terraform, reads WAF logs to classify matches, adds expiring virtual patches for CVEs, and asks before any block-mode change goes live.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- waf-rules Installs to ~/.agents/skills/waf-rules/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install waf-rules The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project waf-rules Installs to .agents/skills/waf-rules/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/waf-rules.zipunzip -o waf-rules.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) waf-rules PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $waf-rules, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes, deploys and tunes web application firewall rules on Cloudflare WAF and AWS WAF (plus ModSecurity or Coraza with the OWASP Core Rule Set) - managed rulesets, custom expressions, rate limiting, bot and geo rules, count-mode rollouts and narrowly scoped exceptions for false positives - managed as Terraform. Use when enabling a WAF, when legitimate requests get 403s from a managed rule, when writing a rule to block an abusive path or virtual-patch a CVE, or when reviewing WAF logs to decide what to tune.
Use it when
- You are enabling a WAF for the first time
- Legitimate users or webhooks are being blocked by a managed rule
- You need a temporary virtual patch for a known CVE
- WAF rules live only in a dashboard and need to move to code
Use something else when
- You are designing per-user or per-key limits inside the API: use api-rate-limiting
- The real fix is in application code such as an injection bug: use sql-injection-prevention
How to check the result
Replayed false-positive requests pass, a harmless known-bad probe to an owned URL is blocked with the expected rule ID in the logs, and 403 rates stay flat for 48 hours after the change.
The mistake it prevents
Fixing a false positive by skipping the entire managed ruleset for a path, which silently removes protection from everything on it.
Done well means: every rule went live in log or count mode first, every block is explained by a rule ID in the logs, false positives are fixed with the narrowest possible exception (one rule, one path, one parameter), and the whole configuration lives in version control. A WAF is a filter in front of the application, never a substitute for fixing the bug.
Before touching rules
- Read
AGENTS.mdand find how the WAF is managed: Terraform (cloudflare_ruleset,aws_wafv2_web_acl), CloudFormation, CDK, or dashboard only. If dashboard only, export the current config before changing anything and propose moving it to code. - Get logs flowing first. Cloudflare: Security Events (Security Analytics) or Logpush of
firewall events. AWS WAF: logging to CloudWatch Logs (log group name must start with
aws-waf-logs-), S3 or Firehose. - Identify the traffic: APIs with JSON bodies, file uploads, rich-text editors, GraphQL, webhooks from third parties. These are the usual false-positive sources.
Production WAF changes affect all traffic. Show the user the plan (terraform plan) and get
explicit confirmation before applying any rule in block mode.
Rollout order
- [ ] 1. Managed rules on in log/count mode; collect at least 7 days including a business peak
- [ ] 2. Review top-matching rules per path; classify true vs false positives
- [ ] 3. Add narrow exceptions for confirmed false positives
- [ ] 4. Switch managed rules to block, one rule group at a time
- [ ] 5. Add custom rules and rate limits (count first, then block)
- [ ] 6. Alert on block-rate spikes and on sudden drops (rule disabled or bypassed)
Cloudflare
Custom rules use the Rules language. Header names in http.request.headers are lowercase.
# provider v4 block syntax; v5 takes rules = [ { ... } ] as a list attribute
resource "cloudflare_ruleset" "custom" {
zone_id = var.zone_id
name = "custom firewall rules"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
description = "Block admin panel outside office and VPN ranges"
expression = "(starts_with(http.request.uri.path, \"/admin\") and not ip.src in $office_ranges)"
action = "block"
}
rules {
description = "Challenge login posts from non-browser clients"
expression = "(http.request.uri.path eq \"/login\" and http.request.method eq \"POST\" and not cf.client.bot)"
action = "managed_challenge"
}
}
- Exceptions to managed rules go in the managed phase (
http_request_firewall_managed) as a rule withaction = "skip"placed before the managed ruleset execution, skipping only the specific rule IDs for the specific path. Skipping the whole ruleset for a path is the common over-broad fix. - The Cloudflare OWASP Core Ruleset scores requests; tune the anomaly score threshold and paranoia level rather than disabling it. Start at paranoia level 1.
- Rate limiting rules (
http_ratelimitphase) key on IP by default; for APIs behind NAT or mobile carriers, key on a header such as an API key or session cookie where the plan allows. logas an action is limited to some plans; on others, test new expressions withmanaged_challengeon a low-traffic path or use Security Analytics to see what would match.- Use the Terraform provider version the repo already pins; resource schemas changed between major versions (v4 to v5), so read the pinned provider docs before writing blocks.
AWS WAF (v2)
rule {
name = "aws-common"
priority = 10
override_action { none {} } # use count {} for the observation period
statement {
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesCommonRuleSet"
rule_action_override { # one noisy rule to count, the rest still block
name = "SizeRestrictions_BODY"
action_to_use { count {} }
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "aws-common"
sampled_requests_enabled = true
}
}
- Narrow exceptions: count the noisy rule, then add a later custom rule that blocks when the
rule’s label is present and the request is not on the excepted path (
label_match_statementcombined with anot+byte_match_statementon the URI). That keeps protection everywhere else. - Body inspection is limited: 8 KB for ALB and AppSync, 16 KB default (configurable up to
64 KB) for CloudFront, API Gateway and some others. Set
oversize_handlingdeliberately;CONTINUElets large payloads skip body inspection. - Useful baseline groups:
AWSManagedRulesCommonRuleSet,AWSManagedRulesKnownBadInputsRuleSet,AWSManagedRulesAmazonIpReputationList, plusSQLiRuleSetand language-specific sets that match the stack. Bot Control and ATP/ACFP groups cost extra per request. - Rate-based rules aggregate over a 1, 2, 5 or 10 minute window and can key on IP, forwarded IP, header, cookie or query argument.
CloudWatch Logs Insights query for tuning:
fields @timestamp, httpRequest.uri, terminatingRuleId, httpRequest.clientIp
| filter action = "BLOCK"
| stats count(*) as hits by terminatingRuleId, httpRequest.uri
| sort hits desc
| limit 50
ModSecurity / Coraza with CRS
Exclusions follow the CRS pattern: ctl:ruleRemoveTargetById for one argument on one path
(in a REQUEST-900 exclusion file), not SecRuleRemoveById globally. Raise the paranoia
level only after PL1 runs clean.
Deciding true vs false positive
| Signal | Likely |
|---|---|
| Match on a known field that holds rich text, code or HTML (editor, comment, template) | False positive: exception for that field on that path |
| Many IPs, normal user agents, authenticated sessions, business hours | False positive |
| Scanner user agents, sequential payloads, one IP across many paths | True positive |
| Matches only from third-party webhook IPs on the webhook path | False positive: verify the sender by signature in the app, exclude the path from body rules |
Virtual patching a CVE
Write a custom rule matching the exploit precondition (path plus the specific parameter shape), deploy in count, confirm it matches the public proof of concept in a test environment and not normal traffic, then block. Record the CVE and an expiry: remove the rule after the real patch ships, or it outlives everyone’s memory of why it exists.
Gotchas
- Rules that trust
X-Forwarded-Forcan be spoofed unless the WAF knows the trusted proxy chain. Use the platform’s client IP field. - Geo-blocking is easy to evade with VPNs; treat it as noise reduction only.
- Blocking returns 403 with a WAF page; API clients see HTML instead of JSON. Configure a custom JSON response for API hostnames.
- A rule that blocks your own health checks or uptime monitors causes a false outage. Exempt them by path and source, not by user agent.
- WAF only sees traffic routed through it. Confirm the origin rejects direct connections (allowlist CDN ranges, authenticated origin pulls, or private origin).
Verification
- Replay sampled false-positive requests against the tuned rule in a test zone or staging ACL: they pass.
- Send a harmless known-bad probe (for example
?q=<script>alert(1)</script>or a CRS test string) to a URL you own: blocked, with the expected rule ID in the logs. - Compare block counts and 403 rates for 48 hours after each change; investigate any spike.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "WAF Rules"
short_description: "Count first, block later, except narrowly"
default_prompt: "$waf-rules Our checkout API gets 403s from AWS WAF since Friday - find the rule and fix it narrowly" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.