Skip to content

Make the OWASP WAF rules actually evaluate (preview mode first) - #153

Merged
rcurranmoz merged 1 commit into
mainfrom
fix/cloud-armor-waf-ordering
Sep 17, 2026
Merged

rcurranmoz merged 1 commit into
mainfrom
fix/cloud-armor-waf-ordering

Conversation

@rcurranmoz

Copy link
Copy Markdown
Collaborator

The three preconfigured WAF rules in hangar-armor have never evaluated a single request.

They sit at priority 2000–2002, below the rate-limit rules. Cloud Armor enforces the first matching rule and stops, and the throttle rules between them match every request. Confirmed in the load-balancer logs while fixing #152 — every request reports enforcedSecurityPolicy priority 1000:

/api/alerts        200  → priority 1000  ACCEPT
/api/fleet/summary 200  → priority 1000  ACCEPT
/docSQL  (scanner) 429  → priority 1000  DENY

That last line is the tell. A scanner walking /cgi-bin, /docSQL and ~60 other paths collects a 429 from the rate limiter, never a 403 from the RFI rule. The rules were dead config, not defence.

The change

Move them to 700–702, above the throttles. That is the entire mechanism — Cloud Armor evaluates ascending, so they have to be below the rate-limit rules numerically to run before them.

Resulting order:

Priority Rule Enforcing?
700 XSS (xss-v33-stable) preview
701 SQLi (sqli-v33-stable) preview
702 RFI (rfi-v33-stable) preview
900 Rate limit /api/* — 1200/60s yes
1000 Rate limit app shell — 3000/60s yes
2147483647 Default allow yes

The separate hangar-runner-armor policy is untouched.

Why preview, and why that makes this safe to merge

This commit is behaviour-neutral by construction. A preview rule logs its match and evaluation continues to the next rule, so the 429 ceilings still apply and nothing new is denied. (The proof that evaluation continues is that LB log entries carry enforcedSecurityPolicy and previewSecurityPolicy as separate fields on the same request.)

That is deliberate rather than timid. Preconfigured CRS rules are well known to false-positive on ordinary traffic, and because these have never been in this dashboard's request path, their real hit rate here is unknown rather than demonstrably zero. Enforcing three untested deny rules on an origin Mozilla staff use daily, in the same change that first makes them reachable, is the wrong order of operations — a false positive would surface as a 403 on a dashboard page with no obvious cause.

The rule bodies are unchanged — same three evaluatePreconfiguredExpr calls — so the only variables here are order and preview.

Measuring, then graduating

previewSecurityPolicy is only populated by preview rules, so this isolates exactly what they would have blocked:

gcloud logging read \
  'resource.type="http_load_balancer" AND jsonPayload.previewSecurityPolicy.outcome="DENY"' \
  --project=relops-dashboard --freshness=7d --limit=200 \
  --format="value(httpRequest.remoteIp,httpRequest.requestUrl,jsonPayload.previewSecurityPolicy.priority)"

Run for at least a week of normal use. If every DENY is a scanner and none is an operator, drop preview = true one rule at a time — sqli last, since it is the most false-positive-prone. If a rule matches legitimate traffic, tune sensitivity or add a rule exclusion rather than deleting it.

Why now

#152 raised the ceilings on the only Cloud Armor rule that actually executes — 100/min → 1200 for /api and 3000 for the app shell. That was the right call for the operator-facing 429, but it does mean less incidental friction for the scanners already probing this origin, and the WAF is the control that is supposed to cover that. It should at least be measured.

Deploy note

Terraform-only; nothing deploys on merge. Needs a manual terraform apply, and CI cannot validate this — the terraform job runs fmt and validate, never plan. Expect 0 to add, 1 to change, 0 to destroy touching only google_compute_security_policy.hangar.

Post-apply, confirm the order and that the three WAF rules report preview: True while the throttles report False:

gcloud compute security-policies describe hangar-armor --project=relops-dashboard \
  --format="table(rules[].priority,rules[].action,rules[].preview)"

Verified: terraform fmt, terraform validate clean.

🤖 Generated with Claude Code

The three preconfigured WAF rules in `hangar-armor` have never evaluated a
request. They sit at priority 2000-2002, below the rate-limit rules, and Cloud
Armor enforces the first matching rule and stops -- the throttle rules between
them match every request. Confirmed in the load-balancer logs while fixing #152:
every request reports `enforcedSecurityPolicy priority 1000`, including a scanner
walking /cgi-bin, /docSQL and ~60 other paths, which collects a 429 from the rate
limiter rather than a 403 from the RFI rule. They were dead config, not defence.

This moves them to 700-702, above the throttles, which is what makes them run.

They land `preview = true`. A preview rule logs its match and evaluation
continues to the next rule, so the 429 ceilings still apply and nothing new is
denied -- this commit is behaviour-neutral by construction. That is deliberate:
preconfigured CRS rules are known to false-positive on ordinary traffic, and
because these have never been in this dashboard's request path, their real hit
rate here is unknown rather than demonstrably zero. Enforcing three untested deny
rules on an origin Mozilla staff use daily, in the same change that first makes
them reachable, would be the wrong order of operations.

The rule bodies are unchanged -- same three `evaluatePreconfiguredExpr` calls --
so the only variables are order and preview. Graduation path, measurement query
and the reason to drop sqli last are in a comment at the rules.

Relevant now rather than later: #152 raised the ceilings on the only Cloud Armor
rule that actually executes, 100/min to 1200 for /api and 3000 for the app shell.
That was the right call for the operator-facing 429, but it does mean less
incidental friction for the scanners already probing this origin, and the WAF is
the control that is supposed to cover that -- so it should at least be measured.

Verified: terraform fmt, validate clean. Rule order is now 700/701/702 WAF
(preview) -> 900 API throttle -> 1000 shell throttle -> default allow. The
separate hangar-runner-armor policy is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rcurranmoz
rcurranmoz merged commit 67311d3 into main Sep 17, 2026
5 checks passed
@rcurranmoz
rcurranmoz deleted the fix/cloud-armor-waf-ordering branch September 17, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant