Skip to main content

Tuning

Most WAF deployments die the same way: someone enables the CRS, it blocks a legitimate request on day two, and the WAF is switched off and never switched back on. Tuning is the work that prevents that, and it is mostly about evidence before enforcement.

The sequence​

  1. Deploy in monitoring mode. block: false. Rules run, events are emitted, nothing is denied.
  2. Collect for a full traffic cycle. A week catches the weekly batch job; a day does not.
  3. Find the noisy rules. Group CloudApimWafTrailEvent by events[].rule_id and sort by count. The top of that list is almost always false positives, not attacks.
  4. Write exclusions for the legitimate traffic, rule by rule.
  5. Re-measure. The remaining block events should be things you are happy to deny.
  6. Arm it. Set block: true.

Reading an event​

{
"@type": "CloudApimWafTrailEvent",
"blocking": false,
"block": { "status": 403, "msg": "SQL Injection Attack Detected via libinjection" },
"events": [
{ "rule_id": 942100, "msg": "SQL Injection Attack Detected via libinjection", "phase": 2 }
]
}
FieldMeaning
blockingWhether the configuration is in blocking mode
blockNon-null when the ruleset reached a deny decision
events[]Every rule that matched, with its id, message and phase

blocking: false with a non-null block is the one to count: this request would have been denied. That number, over real traffic, is the cost of arming the WAF.

Writing exclusions​

An exclusion narrows a rule instead of removing it. Prefer the narrowest form that works.

There is an assistant for this

The tuning assistant generates every form below from an observed match, runs each one against your configuration before offering it, and reports which known attacks stop being caught if you take it. This section is what it generates — worth reading either way, and necessary if you are writing them by hand.

Exclude one parameter from one rule​

The most common fix. A search box legitimately contains SQL-looking text:

SecRuleUpdateTargetById 942100 "!ARGS:search_query"

Exclude a parameter from a whole rule family​

SecRuleUpdateTargetByTag "attack-sqli" "!ARGS:filter_expression"

Disable a rule on one path only​

SecRule REQUEST_URI "@beginsWith /api/reports" \
"id:1900,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

Disable a rule entirely​

SecRuleRemoveById 942100

Last resort. It removes the protection everywhere, for every route sharing the configuration.

Where an exclusion has to sit

It depends on which kind it is, and getting it wrong fails silently rather than loudly.

SecRuleUpdateTargetById, SecRuleUpdateTargetByTag and SecRuleRemoveById are declarations: the engine collects them across the whole configuration, so they work from anywhere — including from a ruleset listed after the one carrying @import_preset crs.

The ctl: forms are different. They are ordinary phase 1 rules that set state the target rule reads afterwards, so they have to be evaluated before it. Put them in a ruleset listed first. Placed after the CRS they are too late for anything the CRS does in phase 1, and they will appear to do nothing.

The tuning assistant handles this for you, and keeps the two kinds in separate rulesets for exactly this reason.

Anomaly scoring, in practice​

The CRS accumulates a score and denies past a threshold, so a single moderate match is not fatal. When tuning, that gives you a second knob: instead of excluding a rule, raise the threshold.

SecAction "id:900110,phase:1,nolog,pass,t:none,setvar:tx.inbound_anomaly_score_threshold=10"

Raising the threshold is blunter than an exclusion — it weakens every rule at once — but it is a reasonable stopgap while you work through a backlog of false positives, and much better than turning the WAF off.

Per-route configurations​

Routes with genuinely different traffic deserve different configurations. A public marketing site and an internal admin API do not need the same exclusions, and sharing one WafConfig means every exclusion you write for one weakens the other.

The cost is more entities to maintain. A reasonable middle ground is one configuration per class of traffic — public, authenticated, internal — rather than one per route.

Checklist before arming​

Or let it measure itself

Learning mode runs this checklist for you: it counts a window, proposes the exclusions that account for it, and refuses to say "safe to arm" until the evidence supports it.

  • Monitoring ran for at least one full traffic cycle
  • The top ten matching rule ids have been reviewed individually
  • Every exclusion is scoped to a parameter or a path, not a global SecRuleRemoveById
  • Every exclusion has been verified to actually take effect — the assistant refuses to save one that does not
  • input_body_limit is set, and paired with a hard body-size limit on the route
  • The remaining would-be blocks have been checked as real attacks
  • Someone knows how to switch block back to false in a hurry