<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Peakhour.IO - OWASP CRS</title><link href="https://www.peakhour.io/" rel="alternate"></link><link href="https://www.peakhour.io/feeds/tag/owasp-crs.atom.xml" rel="self"></link><id>https://www.peakhour.io/</id><updated>2026-07-29T10:00:00+10:00</updated><entry><title>How to Tune Google Cloud Armor WAF Rules</title><link href="https://www.peakhour.io/blog/how-to-tune-google-cloud-armor-waf/" rel="alternate"></link><published>2026-07-29T10:00:00+10:00</published><updated>2026-07-29T10:00:00+10:00</updated><author><name>AC</name></author><id>tag:www.peakhour.io,2026-07-29:/blog/how-to-tune-google-cloud-armor-waf/</id><summary type="html">&lt;p&gt;Tune Cloud Armor preconfigured WAF rules with preview traffic, versioned signatures, sensitivity levels, field exclusions and policy priority.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Cloud Armor evaluates security-policy rules by priority. The request log shows the preview rule that matched and the enforced rule that decided the request.&lt;/p&gt;
&lt;p&gt;An &lt;code&gt;allow&lt;/code&gt; rule at priority 100 accepts the request before a SQL injection rule at priority 1000 can inspect it. A WAF rule in preview records the action it would have taken while an enforced rule decides the request.&lt;/p&gt;
&lt;h2&gt;Preview one WAF rule&lt;/h2&gt;
&lt;p&gt;Cloud Armor preview belongs to individual security-policy rules. The configured action is recorded while policy evaluation continues to an enforced rule. Both results appear in the request log when the preview rule takes priority. &lt;a href="https://docs.cloud.google.com/armor/docs/security-policy-overview#preview-mode"&gt;Google documents this evaluation behaviour and the &lt;code&gt;--preview&lt;/code&gt; flag&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The commands below use a global service-level security policy. Add &lt;code&gt;--region=REGION&lt;/code&gt; to each command when working with a regional policy.&lt;/p&gt;
&lt;p&gt;Create or update the WAF rule with a deny action and preview enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gcloud&lt;span class="w"&gt; &lt;/span&gt;compute&lt;span class="w"&gt; &lt;/span&gt;security-policies&lt;span class="w"&gt; &lt;/span&gt;rules&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--security-policy&lt;span class="w"&gt; &lt;/span&gt;app-policy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--expression&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;evaluatePreconfiguredWaf(&amp;#39;sqli-v422-stable&amp;#39;, {&amp;#39;sensitivity&amp;#39;: 1})&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--action&lt;span class="w"&gt; &lt;/span&gt;deny-403&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--preview&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--description&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;SQLi v4.22 sensitivity 1&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sensitivity 1 is a sensible starting point for most applications. It enables the higher-confidence signatures and usually produces less noise than higher levels. Google recommends starting new preconfigured WAF rules at sensitivity 1 and validating them in preview. &lt;a href="https://docs.cloud.google.com/armor/docs/best-practices#rule_tuning"&gt;The current Cloud Armor best-practices guide gives both recommendations&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Read both policy results in the request log&lt;/h2&gt;
&lt;p&gt;Cloud Armor adds its policy results to the load balancer’s per-request log. Request logging is disabled by default for new backend services, so enable it before starting the preview. Look for &lt;code&gt;previewSecurityPolicy&lt;/code&gt; to see the preview match and &lt;code&gt;enforcedSecurityPolicy&lt;/code&gt; to see the enforced result. The &lt;code&gt;preconfiguredExprIds&lt;/code&gt; array contains every WAF signature that triggered the matching policy rule. &lt;a href="https://docs.cloud.google.com/armor/docs/request-logging#security_policy_log_entries"&gt;Google lists the logging prerequisite and policy fields in the Cloud Armor request-logging reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A Logs Explorer query can isolate the preview rule:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;resource.type=&amp;quot;http_load_balancer&amp;quot;
jsonPayload.previewSecurityPolicy.name=&amp;quot;app-policy&amp;quot;
jsonPayload.previewSecurityPolicy.priority=1000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Group the results by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;preconfiguredExprIds&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;request path and method;&lt;/li&gt;
&lt;li&gt;preview rule priority;&lt;/li&gt;
&lt;li&gt;enforced outcome;&lt;/li&gt;
&lt;li&gt;client or application flow;&lt;/li&gt;
&lt;li&gt;time and release version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use our &lt;a href="/blog/how-to-tune-your-waf/"&gt;general WAF tuning guide&lt;/a&gt; to classify those repeated matches and choose the smallest safe exception.&lt;/p&gt;
&lt;p&gt;Enable verbose logging for a short investigation when the ordinary log shows the signature but omits the input. It adds &lt;code&gt;matchedFieldType&lt;/code&gt;, &lt;code&gt;matchedFieldName&lt;/code&gt; and a short &lt;code&gt;matchedFieldValue&lt;/code&gt; snippet for requests matching preconfigured WAF rules. Those snippets can contain customer or personal data. Google recommends disabling verbose logging during normal operation. &lt;a href="https://docs.cloud.google.com/armor/docs/verbose-logging"&gt;The verbose-logging guide describes the extra fields and privacy risk&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Match the signature ID to the ruleset version&lt;/h2&gt;
&lt;p&gt;The name &lt;code&gt;sqli-v422-stable&lt;/code&gt; identifies the SQL injection ruleset built from CRS 4.22. A signature inside it has the same version in its ID, such as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;owasp-crs-v042200-id942350-sqli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Use the ID written by the policy log. Cloud Armor requires an opted-out signature ID to match the ruleset version, and rejects mismatched versions. &lt;a href="https://docs.cloud.google.com/armor/docs/rule-tuning#opt_out_rule_signatures"&gt;Google shows the versioned naming and requirement in its WAF tuning guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A ruleset upgrade can add or change signatures. Put the new version in preview, compare its matches with the enforced version and promote it after reviewing the result.&lt;/p&gt;
&lt;p&gt;Google also publishes canary rulesets. Its recommended arrangement runs the canary at a higher priority in preview and the stable ruleset at a lower priority in enforcement. The canary records the requests it would deny while the stable rule protects the service. &lt;a href="https://docs.cloud.google.com/armor/docs/best-practices#use_stable_or_canary_rules"&gt;Google describes this stable-and-canary arrangement in its best practices&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Choose the sensitivity level first&lt;/h2&gt;
&lt;p&gt;Sensitivity levels run from 0 to 4. Choosing level 2 enables signatures at levels 1 and 2. Higher levels inspect for more patterns and tend to produce more false positives. Level 0 enables no signatures unless you explicitly opt them in. &lt;a href="https://docs.cloud.google.com/armor/docs/rule-tuning#choose_a_sensitivity_level"&gt;Google maps Cloud Armor sensitivity to CRS paranoia levels in the tuning documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Choose a level that fits the application and the time available to investigate. A public marketing site with conventional inputs may run cleanly at a higher level. An API accepting templates or query fragments may need more work.&lt;/p&gt;
&lt;p&gt;Keep the chosen sensitivity and exclude the named field from the affected signature.&lt;/p&gt;
&lt;h2&gt;Opt out one noisy signature&lt;/h2&gt;
&lt;p&gt;If a signature is irrelevant wherever the policy rule applies, list its versioned ID in &lt;code&gt;opt_out_rule_ids&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;evaluatePreconfiguredWaf(
  &amp;#39;sqli-v422-stable&amp;#39;,
  {
    &amp;#39;sensitivity&amp;#39;: 2,
    &amp;#39;opt_out_rule_ids&amp;#39;: [
      &amp;#39;owasp-crs-v042200-id942350-sqli&amp;#39;
    ]
  }
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The WAF rule runs every SQL injection signature enabled at sensitivity 1 and 2 except the named signature.&lt;/p&gt;
&lt;p&gt;Use an opt-out after reviewing where the policy rule applies. A rule matching every request to a shared backend gives the exception the same reach.&lt;/p&gt;
&lt;h2&gt;Enable selected signatures at sensitivity 0&lt;/h2&gt;
&lt;p&gt;Set sensitivity to 0 when the rule needs only a few named signatures:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;evaluatePreconfiguredWaf(
  &amp;#39;xss-v422-stable&amp;#39;,
  {
    &amp;#39;sensitivity&amp;#39;: 0,
    &amp;#39;opt_in_rule_ids&amp;#39;: [
      &amp;#39;owasp-crs-v042200-id941150-xss&amp;#39;
    ]
  }
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cloud Armor permits &lt;code&gt;opt_in_rule_ids&lt;/code&gt; only at sensitivity 0. Google recommends this approach when you want fewer signatures than you would otherwise opt out. &lt;a href="https://docs.cloud.google.com/armor/docs/configure-waf#configure_waf_rules"&gt;The setup guide documents the constraint and provides current CLI examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;An opt-in rule enables only the named signatures for the traffic matched by that policy rule. Record why each signature was selected and when to review the set.&lt;/p&gt;
&lt;h2&gt;Exclude the field that carries legitimate content&lt;/h2&gt;
&lt;p&gt;A request-field exclusion keeps a signature active and removes a selected input from its inspection.&lt;/p&gt;
&lt;p&gt;Cloud Armor can exclude named header values, cookie values, query-parameter values and URIs from an entire preconfigured ruleset or selected signatures. Header, cookie and query-parameter names remain inspected when their values are excluded. URI exclusions remove the matching URI from inspection for the selected target. &lt;a href="https://docs.cloud.google.com/armor/docs/rule-tuning#exclude_request_fields_from_inspection"&gt;Google documents each field’s behaviour and available match operators&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Suppose the cookie &lt;code&gt;analytics_id&lt;/code&gt; repeatedly triggers signature &lt;code&gt;942100&lt;/code&gt;. Add an exclusion to the existing SQLi policy rule:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gcloud&lt;span class="w"&gt; &lt;/span&gt;compute&lt;span class="w"&gt; &lt;/span&gt;security-policies&lt;span class="w"&gt; &lt;/span&gt;rules&lt;span class="w"&gt; &lt;/span&gt;add-preconfig-waf-exclusion&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--security-policy&lt;span class="w"&gt; &lt;/span&gt;app-policy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--target-rule-set&lt;span class="w"&gt; &lt;/span&gt;sqli-v422-stable&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--target-rule-ids&lt;span class="w"&gt; &lt;/span&gt;owasp-crs-v042200-id942100-sqli&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--request-cookie-to-exclude&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;op=EQUALS,val=analytics_id&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The signature continues to inspect the cookie name and every other request field, excluding only the value of &lt;code&gt;analytics_id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Request query parameters need one extra check. Cloud Armor rebuilds the URI and request line after removing an excluded query parameter. A signature that inspects the entire &lt;code&gt;REQUEST_BODY&lt;/code&gt; continues to inspect it, so the same parameter can trigger a body-wide signature. &lt;a href="https://docs.cloud.google.com/armor/docs/rule-tuning#request_query_parameters"&gt;Google calls out this behaviour in the query-parameter exclusion documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A policy rule with request-field exclusions cannot use the &lt;code&gt;allow&lt;/code&gt; action. Use the WAF’s deny action and let unmatched requests continue through the policy. &lt;a href="https://docs.cloud.google.com/armor/docs/waf-rules#limitations"&gt;This restriction appears in the current preconfigured WAF limitations&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Apply the tuned WAF rule to one route&lt;/h2&gt;
&lt;p&gt;Cloud Armor uses Common Expression Language expressions for advanced matches. Combine the route condition and WAF evaluation in one rule when an exception belongs to one application area:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;request.path.startsWith(&amp;#39;/api/messages&amp;#39;) &amp;amp;&amp;amp;
evaluatePreconfiguredWaf(&amp;#39;sqli-v422-stable&amp;#39;, {&amp;#39;sensitivity&amp;#39;: 1})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create a separate SQLi rule for the rest of the application and exclude &lt;code&gt;/api/messages&lt;/code&gt; from its expression. The route is then evaluated once under the tuned rule.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;!request.path.startsWith(&amp;#39;/api/messages&amp;#39;) &amp;amp;&amp;amp;
evaluatePreconfiguredWaf(&amp;#39;sqli-v422-stable&amp;#39;, {&amp;#39;sensitivity&amp;#39;: 1})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Priority decides which matching rule takes effect. A smaller number has higher priority. An earlier &lt;code&gt;allow&lt;/code&gt; rule ends evaluation for the matching request, so later WAF rules never see it. &lt;a href="https://docs.cloud.google.com/armor/docs/best-practices#test_your_logic"&gt;Google explains the evaluation order and demonstrates the effect of an early allow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Replay three requests when testing route logic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a legitimate request containing the known false-positive value;&lt;/li&gt;
&lt;li&gt;an attack string in the excluded field;&lt;/li&gt;
&lt;li&gt;an attack string in another field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The legitimate request should pass. The attack string in the excluded field shows the exposure created by the exception. The attack string in another field should still trigger the WAF rule.&lt;/p&gt;
&lt;h2&gt;Set the request-body inspection limit&lt;/h2&gt;
&lt;p&gt;Cloud Armor inspects a configurable prefix of the request body: 8, 16, 32, 48 or 64 kB. The limit applies across the preconfigured WAF rules in the policy, and 64 kB is the maximum. Content after the limit can reach the backend without WAF body-signature inspection. &lt;a href="https://docs.cloud.google.com/armor/docs/configure-waf#update_inspection_limit_for_preconfigured_waf_rules"&gt;Google documents the available sizes and provides the policy update command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Choose the size from the requests the application accepts. Check uploads, GraphQL, JSON APIs and form submissions separately. Google supports parsed URL-encoded and JSON bodies; other content and encodings, including XML, gzip, UTF-16 and multipart form data, receive rules over raw data. &lt;a href="https://docs.cloud.google.com/armor/docs/security-policy-overview#request_body_inspection_limitation"&gt;The security-policy overview describes the current body parsing behaviour&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Verbose logs include &lt;code&gt;inspectedBodySize&lt;/code&gt;, which helps confirm the active limit. Decide how the application handles larger bodies. Google suggests a separate content-length rule when the application can safely reject requests beyond the inspection limit, though &lt;code&gt;Content-Length&lt;/code&gt; deserves the same validation you would apply to any client-supplied header.&lt;/p&gt;
&lt;h2&gt;Identify who manages the policy&lt;/h2&gt;
&lt;p&gt;Service-level backend security policies attach to backend services. Send their false positives to the team that manages the backend policy.&lt;/p&gt;
&lt;p&gt;Hierarchical policies attach at the organization, folder or project level and flow down to projects. Send inherited false positives to the team that manages the hierarchical policy.&lt;/p&gt;
&lt;p&gt;Creating a hierarchical security policy requires Cloud Armor Enterprise Paygo and can enrol the project automatically. &lt;a href="https://docs.cloud.google.com/armor/docs/hierarchical-policies-overview"&gt;Google documents the commercial requirement and ownership model in the hierarchical policy guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Include the matched policy name and priority in each false-positive report. Give the policy team the field, route and application behaviour needed to review the exception.&lt;/p&gt;
&lt;p&gt;Hierarchical and service-level policies support different feature sets. Check the current feature table before moving a tuned rule between them. &lt;a href="https://docs.cloud.google.com/armor/docs/security-policy-overview#types_of_security_policies"&gt;Google maintains the comparison in its security-policy overview&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Disable preview after reviewing the matches&lt;/h2&gt;
&lt;p&gt;Run normal traffic, scheduled jobs and important application flows through the preview rule. Classify each recurring signature and field, then disable preview:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gcloud&lt;span class="w"&gt; &lt;/span&gt;compute&lt;span class="w"&gt; &lt;/span&gt;security-policies&lt;span class="w"&gt; &lt;/span&gt;rules&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--security-policy&lt;span class="w"&gt; &lt;/span&gt;app-policy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--no-preview
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cloud Armor rule changes usually take several minutes to propagate. Watch the enforced policy outcome after the change and replay the requests used during tuning. &lt;a href="https://docs.cloud.google.com/armor/docs/waf-rules#limitations"&gt;Google lists propagation time among the preconfigured WAF limitations&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Keep the previous expression and exclusions in version control. Record the policy, priority, ruleset version, sensitivity, signature IDs, excluded fields and test requests.&lt;/p&gt;</content><category term="Application Security"></category><category term="WAF"></category><category term="Google Cloud Armor"></category><category term="Google Cloud"></category><category term="OWASP CRS"></category><category term="Application Security"></category><category term="False Positives"></category><category term="WAF Tuning"></category></entry><entry><title>How to Tune Cloudflare WAF When Rule 949110 Blocks a Request</title><link href="https://www.peakhour.io/blog/how-to-tune-cloudflare-waf/" rel="alternate"></link><published>2026-07-29T09:45:00+10:00</published><updated>2026-07-29T09:45:00+10:00</updated><author><name>AC</name></author><id>tag:www.peakhour.io,2026-07-29:/blog/how-to-tune-cloudflare-waf/</id><summary type="html">&lt;p&gt;Trace a Cloudflare OWASP anomaly-score block to the rules that raised the score, then choose the right override or exception.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Rule &lt;code&gt;949110&lt;/code&gt; blocked the request.&lt;/p&gt;
&lt;p&gt;In Cloudflare's OWASP managed ruleset, &lt;code&gt;949110: Inbound Anomaly Score Exceeded&lt;/code&gt; checks the total score produced by earlier detections. The rules that raised the score explain the block.&lt;/p&gt;
&lt;p&gt;Cloudflare puts that answer in Additional logs.&lt;/p&gt;
&lt;h2&gt;Start with the blocked event&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;Security &amp;gt; Events&lt;/strong&gt; in the Cloudflare dashboard and filter to the hostname, path, action and time of the request.&lt;/p&gt;
&lt;p&gt;Security Events shows traffic that Cloudflare acted on or flagged. Its top-level views group events by fields such as action, host, country and service. The sampled log beneath them contains a subset of individual security events, so a busy time range may hide the request you need. Narrowing the time range reduces sampling. &lt;a href="https://developers.cloudflare.com/waf/analytics/security-events/"&gt;Cloudflare documents the Security Events views and sampling behaviour&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Capture the details that identify the request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ray ID;&lt;/li&gt;
&lt;li&gt;timestamp;&lt;/li&gt;
&lt;li&gt;hostname and path;&lt;/li&gt;
&lt;li&gt;method;&lt;/li&gt;
&lt;li&gt;WAF service and action;&lt;/li&gt;
&lt;li&gt;displayed rule ID and description;&lt;/li&gt;
&lt;li&gt;the application result reported by the user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One request can produce more than one security event. Keep the Ray ID and timestamp together while you follow the detections.&lt;/p&gt;
&lt;p&gt;If the displayed description is &lt;code&gt;949110: Inbound Anomaly Score Exceeded&lt;/code&gt;, open &lt;strong&gt;Additional logs&lt;/strong&gt;. Cloudflare places the OWASP rules triggered by that request there. &lt;a href="https://developers.cloudflare.com/waf/analytics/security-events/"&gt;The Security Events documentation describes those contributing OWASP records&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Additional logs deserve a manual check in the dashboard. Cloudflare says triggered OWASP rules shown there are absent from exported Security Events JSON. A log export containing only the final event can therefore leave out the detections that built the anomaly score. &lt;a href="https://developers.cloudflare.com/waf/analytics/security-events/"&gt;The Security Events documentation records that export limitation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Find the rule that raised the score&lt;/h2&gt;
&lt;p&gt;Additional logs may show one contributing rule or several. Record each rule's description and Cloudflare rule ID.&lt;/p&gt;
&lt;p&gt;The description may begin with a familiar CRS-style number. Cloudflare's API and exception controls use Cloudflare's opaque rule ID. Use the identifier displayed by Cloudflare when you configure the rule.&lt;/p&gt;
&lt;p&gt;Group repeated matches by rule, field and route, then review each group with the application owner. A single support message containing SQL syntax may be legitimate. Different paths or different rules belong in separate investigations. The &lt;a href="/blog/how-to-tune-your-waf/"&gt;general WAF tuning guide&lt;/a&gt; explains how to classify each group and record an exception.&lt;/p&gt;
&lt;p&gt;Ask the developer who owns the route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what input the endpoint accepts;&lt;/li&gt;
&lt;li&gt;whether the request was parsed successfully;&lt;/li&gt;
&lt;li&gt;where the matching value is stored or rendered;&lt;/li&gt;
&lt;li&gt;which validation runs at the application;&lt;/li&gt;
&lt;li&gt;whether the field can carry executable syntax by design.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Choose an override or an exception&lt;/h2&gt;
&lt;p&gt;An &lt;strong&gt;override&lt;/strong&gt; changes the configuration of a managed rule or ruleset across that deployment. You can change an action, disable selected rules or adjust settings such as the OWASP anomaly score threshold and paranoia level.&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;exception&lt;/strong&gt; matches a set of requests and skips selected managed rules for those requests. Its expression can use the host, URI path, method and other Cloudflare Rules language fields.&lt;/p&gt;
&lt;p&gt;Use an override when the rule itself needs a different action across the deployment. Use an exception when a known application route needs different treatment.&lt;/p&gt;
&lt;p&gt;Cloudflare allows exceptions to skip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every remaining managed rule;&lt;/li&gt;
&lt;li&gt;one or more managed rulesets;&lt;/li&gt;
&lt;li&gt;one or more rules inside managed rulesets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Select the contributing rule. A whole-ruleset exception removes far more inspection from the matching request. &lt;a href="https://developers.cloudflare.com/waf/managed-rules/waf-exceptions/"&gt;Cloudflare documents the three exception types and their order of precedence&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a false positive confined to message creation, the expression could be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;http.host eq &amp;quot;api.example.com&amp;quot;
and http.request.uri.path eq &amp;quot;/v1/messages&amp;quot;
and http.request.method eq &amp;quot;POST&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Choose the Cloudflare rule ID found in Additional logs as the rule to skip.&lt;/p&gt;
&lt;p&gt;Cloudflare skips that rule for the complete matching request. Another field on &lt;code&gt;POST /v1/messages&lt;/code&gt; can trigger the same rule and receive the same exception. Record that consequence.&lt;/p&gt;
&lt;h2&gt;Place the exception before the managed ruleset&lt;/h2&gt;
&lt;p&gt;An exception works only on managed-ruleset execution rules listed after it in the same phase. Put the skip rule before the rule that executes the OWASP managed ruleset. &lt;a href="https://developers.cloudflare.com/waf/managed-rules/waf-exceptions/define-api/"&gt;Cloudflare's API documentation states that skip rules affect only later execute rules&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Cloudflare can run managed rulesets at account level on Enterprise plans and at zone level. Account-level rulesets execute before zone-level rulesets. An account-level exception affects account-level rules, while a zone-level exception affects zone-level rules. A zone exception cannot prevent an account-level rule from matching first. &lt;a href="https://developers.cloudflare.com/waf/managed-rules/waf-exceptions/"&gt;Cloudflare documents this scope and evaluation order&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Find the execute rule that produced the event. Add the exception at that level and before that execution. If the OWASP ruleset runs at both levels, inspect both deployments and decide which one needs the exception.&lt;/p&gt;
&lt;p&gt;Exceptions take priority over overrides. &lt;a href="https://developers.cloudflare.com/waf/account/managed-rulesets/"&gt;Cloudflare records that precedence in its managed-ruleset documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Test the exception through Cloudflare&lt;/h2&gt;
&lt;p&gt;Send the original legitimate request through the hostname proxied by Cloudflare. An origin-only replay bypasses the configuration you are testing.&lt;/p&gt;
&lt;p&gt;Use a small test set:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the legitimate request on the exempt route;&lt;/li&gt;
&lt;li&gt;the same input on another route;&lt;/li&gt;
&lt;li&gt;a safe request that triggers the contributing rule on the exempt route;&lt;/li&gt;
&lt;li&gt;the same test outside the exception;&lt;/li&gt;
&lt;li&gt;normal traffic that should pass unchanged.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Check Security Events after each request. Confirm the Ray ID, action, contributing rules and final result.&lt;/p&gt;
&lt;p&gt;The safe trigger on the exempt route shows what the exception allows. The same trigger outside the exception should still raise the contributing rule and reach &lt;code&gt;949110&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cloudflare evaluates security products and rules in a defined sequence, and a terminating action stops later evaluation. An earlier custom-rule Block or Managed Challenge can determine the result before Managed Rules run. &lt;a href="https://developers.cloudflare.com/waf/concepts/"&gt;Cloudflare's WAF concepts guide describes this rule execution behaviour&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Set the anomaly threshold from real traffic&lt;/h2&gt;
&lt;p&gt;The OWASP anomaly threshold is the cumulative request score at which &lt;code&gt;949110&lt;/code&gt; applies the configured action. Raising it moves the block point for every request covered by that deployment.&lt;/p&gt;
&lt;p&gt;Use Security Events to see which rules contribute to normal requests before changing the threshold. A repeated false positive from one rule and one endpoint points towards an exception. A broad pattern of low-confidence detections may justify a threshold or paranoia-level review.&lt;/p&gt;
&lt;p&gt;Cloudflare supports overrides for the full ruleset, rule tags and individual rules. Tag and ruleset overrides can also affect future rules that Cloudflare adds under those selections. Individual rule overrides target the rule IDs you chose. &lt;a href="https://developers.cloudflare.com/waf/account/managed-rulesets/"&gt;The managed-ruleset documentation explains these override levels&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Test a threshold change with the same care as a rule exception. It changes when &lt;code&gt;949110&lt;/code&gt; acts across all covered traffic.&lt;/p&gt;
&lt;h2&gt;Record the exception and its rule IDs&lt;/h2&gt;
&lt;p&gt;Document:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the Ray IDs and event times reviewed;&lt;/li&gt;
&lt;li&gt;the contributing Cloudflare rule IDs and descriptions;&lt;/li&gt;
&lt;li&gt;the anomaly threshold and paranoia level;&lt;/li&gt;
&lt;li&gt;the exception expression;&lt;/li&gt;
&lt;li&gt;the account or zone deployment it belongs to;&lt;/li&gt;
&lt;li&gt;its position before the managed-ruleset execute rule;&lt;/li&gt;
&lt;li&gt;what the selected rule stops inspecting;&lt;/li&gt;
&lt;li&gt;the developer who confirmed the application behaviour;&lt;/li&gt;
&lt;li&gt;the requests used for verification;&lt;/li&gt;
&lt;li&gt;the review date.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Recheck the exception after application changes and Cloudflare managed-rule updates. A new route can need different conditions. A revised rule can remove the original false positive.&lt;/p&gt;
&lt;p&gt;Rule &lt;code&gt;949110&lt;/code&gt; applies the configured action when the anomaly score crosses the threshold. Tune the rules that supplied the false-positive score.&lt;/p&gt;</content><category term="Application Security"></category><category term="Cloudflare WAF"></category><category term="WAF"></category><category term="OWASP CRS"></category><category term="Application Security"></category><category term="False Positives"></category><category term="WAF Tuning"></category></entry><entry><title>How to Tune Coraza on Caddy</title><link href="https://www.peakhour.io/blog/how-to-tune-coraza-waf/" rel="alternate"></link><published>2026-07-29T09:00:00+10:00</published><updated>2026-07-29T09:00:00+10:00</updated><author><name>AC</name></author><id>tag:www.peakhour.io,2026-07-29:/blog/how-to-tune-coraza-waf/</id><summary type="html">&lt;p&gt;Run Coraza and OWASP CRS on Caddy in DetectionOnly, read the audit log and limit each exclusion to the field and route that need it.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Coraza sits inside another server. The connector presents each request to Coraza, Coraza runs the rules and the connector applies the result.&lt;/p&gt;
&lt;p&gt;Handler order decides whether Coraza sees the request in time. Body handling decides which fields the rules can inspect. The logging phase decides what you can investigate later.&lt;/p&gt;
&lt;p&gt;The example deployment runs Coraza as Caddy middleware with OWASP Core Rule Set. The same CRS tuning principles apply to other Coraza connectors. Their installation, request handling and logging need their own tests.&lt;/p&gt;
&lt;h2&gt;Put Coraza in the request path&lt;/h2&gt;
&lt;p&gt;The official Caddy module is compiled into Caddy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;xcaddy&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;github.com/corazawaf/coraza-caddy/v2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The module documentation requires &lt;code&gt;order coraza_waf first&lt;/code&gt; in the global options block, which places Coraza at the start of Caddy's HTTP handler order.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coraza_waf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;coraza_waf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;load_owasp_crs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;directives&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;coraza&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;recommended&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecRuleEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DetectionOnly&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecAuditEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RelevantOnly&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecAuditLog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;coraza&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecAuditLogFormat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecAuditLogType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;SecAuditLogParts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ABFHZ&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;crs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;owasp_crs&lt;/span&gt;&lt;span class="o"&gt;/*.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;reverse_proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Caddy connector's &lt;a href="https://github.com/corazawaf/coraza-caddy#using-owasp-core-ruleset"&gt;&lt;code&gt;load_owasp_crs&lt;/code&gt; option exposes the embedded Coraza CRS files&lt;/a&gt;. The three &lt;code&gt;Include&lt;/code&gt; lines then load the recommended Coraza configuration, the CRS setup and the rules.&lt;/p&gt;
&lt;p&gt;Coraza also supports &lt;a href="https://www.coraza.io/docs/tutorials/coreruleset/"&gt;CRS files installed on disk&lt;/a&gt;. A file-based installation loads the Coraza configuration first, &lt;code&gt;crs-setup.conf&lt;/code&gt; second and the CRS rule files last. Choose one source for CRS and record its version so you can compare rule changes during an upgrade.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.coraza.io/docs/seclang/directives/#secruleengine"&gt;&lt;code&gt;SecRuleEngine DetectionOnly&lt;/code&gt;&lt;/a&gt; runs the rules and suppresses disruptive actions such as &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;drop&lt;/code&gt; and &lt;code&gt;redirect&lt;/code&gt;. Legitimate requests can complete while you collect matches. Coraza sets &lt;a href="https://www.coraza.io/docs/seclang/directives/#secrequestbodylimitaction"&gt;&lt;code&gt;SecRequestBodyLimitAction&lt;/code&gt; to &lt;code&gt;ProcessPartial&lt;/code&gt;&lt;/a&gt; in this mode. Watch &lt;a href="https://www.coraza.io/docs/seclang/variables/#inbound_data_error"&gt;&lt;code&gt;INBOUND_DATA_ERROR&lt;/code&gt;&lt;/a&gt; because part of an oversized body may remain uninspected.&lt;/p&gt;
&lt;p&gt;Test the Caddy configuration before reloading it. Then send a safe request that triggers a known CRS rule through the public listener. Confirm that the Coraza event appears and the request reaches the upstream in &lt;code&gt;DetectionOnly&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Configure the Coraza audit log&lt;/h2&gt;
&lt;p&gt;The Caddy access log tells you that a request happened. The Coraza audit log tells you which WAF rule matched.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.coraza.io/docs/seclang/directives/#secauditengine"&gt;&lt;code&gt;SecAuditEngine RelevantOnly&lt;/code&gt;&lt;/a&gt; records transactions that produced a warning or error, along with response statuses selected by &lt;code&gt;SecAuditLogRelevantStatus&lt;/code&gt;. CRS rules normally carry the &lt;code&gt;auditlog&lt;/code&gt; action, so their matches can enter the audit log.&lt;/p&gt;
&lt;p&gt;The example uses &lt;a href="https://www.coraza.io/docs/seclang/directives/#secauditlogformat"&gt;Coraza's JSON audit format&lt;/a&gt; and leaves request-body part &lt;code&gt;C&lt;/code&gt; out of &lt;code&gt;SecAuditLogParts&lt;/code&gt;. Request bodies often contain passwords, personal information and customer text. Add body logging only for a controlled investigation, then remove it.&lt;/p&gt;
&lt;p&gt;The serial audit log in the example is convenient during initial tuning. &lt;a href="https://www.coraza.io/docs/seclang/directives/#secauditlogtype"&gt;Coraza warns that serial logging can slow the server&lt;/a&gt; because transactions share one file.&lt;/p&gt;
&lt;p&gt;Concurrent, HTTPS and syslog outputs suit deployments that need another destination. Send the records to a protected log system with rotation, retention limits and restricted access.&lt;/p&gt;
&lt;p&gt;Start each investigation with these fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request time and transaction ID;&lt;/li&gt;
&lt;li&gt;hostname, method and URL;&lt;/li&gt;
&lt;li&gt;rule ID and message;&lt;/li&gt;
&lt;li&gt;CRS tags such as &lt;code&gt;attack-sqli&lt;/code&gt; or &lt;code&gt;attack-xss&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;matched variable name;&lt;/li&gt;
&lt;li&gt;matched data;&lt;/li&gt;
&lt;li&gt;request and response status.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The variable name identifies the input to investigate. &lt;code&gt;ARGS:message&lt;/code&gt; identifies an application field. &lt;code&gt;REQUEST_COOKIES:campaign&lt;/code&gt; identifies a cookie.&lt;/p&gt;
&lt;p&gt;Handle matched data as customer data. Keep enough to reproduce the result, then retain the rule ID, field name and explanation in the permanent exception record.&lt;/p&gt;
&lt;h2&gt;Check what Coraza parsed&lt;/h2&gt;
&lt;p&gt;CRS can inspect only the fields Coraza extracted from the request.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.coraza.io/docs/seclang/directives/#secrequestbodyaccess"&gt;&lt;code&gt;SecRequestBodyAccess On&lt;/code&gt;&lt;/a&gt; enables body buffering and processing. The selected processor determines which collections phase 2 rules can inspect. URL-encoded forms, multipart forms, JSON and XML produce different variables.&lt;/p&gt;
&lt;p&gt;For an unexpected body match, record:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the request &lt;code&gt;Content-Type&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQBODY_PROCESSOR&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQBODY_ERROR&lt;/code&gt; and &lt;code&gt;REQBODY_ERROR_MSG&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INBOUND_DATA_ERROR&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the matched variable name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://www.coraza.io/docs/reference/body-processing/"&gt;Coraza's current body-processing documentation&lt;/a&gt; shows processor selection by content type. The &lt;a href="https://www.coraza.io/docs/seclang/actions/#ctl"&gt;&lt;code&gt;ctl:requestBodyProcessor&lt;/code&gt; reference&lt;/a&gt; still describes explicit phase 1 selection for JSON and XML. Check &lt;code&gt;REQBODY_PROCESSOR&lt;/code&gt; on the Coraza version and connector you run, then select the processor explicitly when the expected processor is missing.&lt;/p&gt;
&lt;p&gt;A parsing error deserves its own investigation. Coraza documents the risk plainly: the application may accept a body that Coraza's parser rejected. Compare the application's parser with Coraza using the same bytes. Fix a wrong content type or processor selection before writing a rule exclusion.&lt;/p&gt;
&lt;p&gt;Body limits need the same care. &lt;a href="https://www.coraza.io/docs/seclang/directives/#secrequestbodylimitaction"&gt;&lt;code&gt;SecRequestBodyLimitAction ProcessPartial&lt;/code&gt;&lt;/a&gt; inspects the bytes that fit within the limit and sets &lt;a href="https://www.coraza.io/docs/seclang/variables/#inbound_data_error"&gt;&lt;code&gt;INBOUND_DATA_ERROR&lt;/code&gt;&lt;/a&gt;. Raise a limit when the application legitimately accepts larger bodies and the deployment can buffer them safely. Keep an alert for the limit condition so partial inspection remains visible.&lt;/p&gt;
&lt;h2&gt;Load exclusions in the correct order&lt;/h2&gt;
&lt;p&gt;CRS provides two places for local exclusions.&lt;/p&gt;
&lt;p&gt;Use the &lt;a href="/blog/how-to-tune-your-waf/"&gt;general WAF tuning guide&lt;/a&gt; to classify the match and define the exception. Load that exception according to the CRS rule order below.&lt;/p&gt;
&lt;p&gt;A runtime exclusion runs for matching transactions. Load it before CRS so it can change the transaction before the affected rule executes. CRS includes &lt;code&gt;REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example&lt;/code&gt; for this purpose.&lt;/p&gt;
&lt;p&gt;A configure-time exclusion changes a loaded rule. Load it after CRS so the rule already exists. CRS includes &lt;code&gt;RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example&lt;/code&gt; for these changes.&lt;/p&gt;
&lt;p&gt;Keep both files outside the CRS distribution. An upgrade can then replace CRS without replacing your application decisions.&lt;/p&gt;
&lt;p&gt;The order in a Caddy deployment looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;directives&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;@coraza&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;recommended&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SecRuleEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DetectionOnly&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;@crs&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;coraza&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;REQUEST&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;EXCLUSION&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;RULES&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;BEFORE&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;CRS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;Include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;@owasp_crs&lt;/span&gt;&lt;span class="cm"&gt;/*.conf&lt;/span&gt;
&lt;span class="cm"&gt;    Include /etc/coraza/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf&lt;/span&gt;
&lt;span class="cm"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;load_owasp_crs&lt;/code&gt; enabled, the &lt;a href="https://github.com/corazawaf/coraza-caddy/blob/82f09c076b75f0096b8c9ebc58e1ce53e2947198/coraza.go#L106-L108"&gt;Caddy connector merges the embedded CRS filesystem with the operating-system filesystem&lt;/a&gt;. The configuration can include embedded &lt;code&gt;@&lt;/code&gt; files and local &lt;code&gt;/etc/coraza&lt;/code&gt; exclusion files.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://coreruleset.org/docs/2-how-crs-works/2-3-false-positives-and-tuning/"&gt;CRS tuning guide explains the before-and-after placement&lt;/a&gt; and the available exclusion actions.&lt;/p&gt;
&lt;h2&gt;Exclude the field that caused the match&lt;/h2&gt;
&lt;p&gt;Suppose &lt;code&gt;POST /api/jobs&lt;/code&gt; accepts a JSON array. Rule 932125 repeatedly matches legitimate text in each &lt;code&gt;jobDescription&lt;/code&gt; field. In this example, the audit log reports variable names such as &lt;code&gt;ARGS:json.0.jobdescription&lt;/code&gt; and &lt;code&gt;ARGS:json.1.jobdescription&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Coraza accepts a regular expression for the collection key in &lt;code&gt;ctl:ruleRemoveTargetById&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf&lt;/span&gt;
&lt;span class="c"&gt;# Local rule ID 10010 must be unique in this deployment.&lt;/span&gt;
&lt;span class="nb"&gt;SecRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;REQUEST_FILENAME&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;@streq /api/jobs&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;id:10010,phase:1,pass,nolog,t:none,chain&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;SecRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;REQUEST_METHOD&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;@streq POST&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;t:none,ctl:ruleRemoveTargetById=932125;ARGS:/^json\.\d+\.jobdescription$/&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This local rule runs only for &lt;code&gt;POST /api/jobs&lt;/code&gt;. Rule 932125 continues to inspect every other field in those requests and every field on other routes.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/pattern/&lt;/code&gt; form is a Coraza feature. The &lt;a href="https://www.coraza.io/docs/seclang/actions/#ctl"&gt;Coraza action reference&lt;/a&gt; states that collection-key matching is case-insensitive because Coraza lowercases variable names. Use an exact key such as &lt;code&gt;ARGS:message&lt;/code&gt; when the application has one fixed field. A regular expression earns its place when the application produces a documented family of field names.&lt;/p&gt;
&lt;p&gt;A field can trigger several rules in one attack class. Coraza also supports &lt;code&gt;ctl:ruleRemoveTargetByTag&lt;/code&gt;, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;ctl&lt;/span&gt;:ruleRemoveTargetByTag=attack-sqli;ARGS:message
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The exception removes the named field from every rule carrying the literal &lt;code&gt;attack-sqli&lt;/code&gt; tag. Record that scope and confirm that the application protects the field through parameterised queries and suitable validation.&lt;/p&gt;
&lt;p&gt;Configure-time exclusions suit fields with the same meaning everywhere:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf&lt;/span&gt;
&lt;span class="nb"&gt;SecRuleUpdateTargetById&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;942100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!REQUEST_COOKIES:campaign&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This removes one named cookie from rule 942100 throughout the configuration. A route-specific runtime exclusion is the better starting point for an application field.&lt;/p&gt;
&lt;h2&gt;Check where a chained rule reads the field&lt;/h2&gt;
&lt;p&gt;Some CRS detections are chained rules. The first rule starts the chain and later rules add conditions.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://coreruleset.org/docs/2-how-crs-works/2-3-false-positives-and-tuning/"&gt;CRS tuning documentation&lt;/a&gt; states that target exclusions by ID affect only the first rule in a chain. An exclusion will have no effect when the field is inspected by a later chain member. Removing the complete rule may be the available answer.&lt;/p&gt;
&lt;p&gt;Find the rule in the CRS version you deployed. Check where the target appears in the chain. Then replay the request against that exact version. The rule source and replay result settle the question.&lt;/p&gt;
&lt;h2&gt;Replay the request before and after&lt;/h2&gt;
&lt;p&gt;Save the legitimate request that produced the false positive. Preserve its method, path, query string, content type, important headers and body bytes. Replace credentials and personal data with values that reproduce the same match.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;--silent&lt;span class="w"&gt; &lt;/span&gt;--show-error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dump-header&lt;span class="w"&gt; &lt;/span&gt;before.headers&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--output&lt;span class="w"&gt; &lt;/span&gt;before.body&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--request&lt;span class="w"&gt; &lt;/span&gt;POST&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--header&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--data-binary&lt;span class="w"&gt; &lt;/span&gt;@false-positive.json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;https://api.example.com/api/jobs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Run it once in &lt;code&gt;DetectionOnly&lt;/code&gt; before adding the exclusion. Confirm that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the application returns its normal response;&lt;/li&gt;
&lt;li&gt;the audit log contains the expected rule ID;&lt;/li&gt;
&lt;li&gt;the log names the expected field;&lt;/li&gt;
&lt;li&gt;the request body has no parser or size error.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add the exclusion, validate the Caddy configuration and reload it. Send the same request bytes again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;--silent&lt;span class="w"&gt; &lt;/span&gt;--show-error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dump-header&lt;span class="w"&gt; &lt;/span&gt;after.headers&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--output&lt;span class="w"&gt; &lt;/span&gt;after.body&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--request&lt;span class="w"&gt; &lt;/span&gt;POST&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--header&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--data-binary&lt;span class="w"&gt; &lt;/span&gt;@false-positive.json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;https://api.example.com/api/jobs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The application response should stay the same. The false-positive match should disappear for the excluded field.&lt;/p&gt;
&lt;p&gt;Now send a control request that places a known CRS test value in another field on the same route. Rule 932125 should still inspect that field. Send the original test value to another route as well. The rule should still inspect it there.&lt;/p&gt;
&lt;p&gt;Expect these results:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the legitimate request works;&lt;/li&gt;
&lt;li&gt;the rule still reads neighbouring fields;&lt;/li&gt;
&lt;li&gt;the rule still runs on other routes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keep the request fixtures in a protected test repository. Run them after Coraza, CRS, Caddy or application upgrades.&lt;/p&gt;
&lt;h2&gt;Switch Coraza from DetectionOnly to On&lt;/h2&gt;
&lt;p&gt;Switch &lt;code&gt;SecRuleEngine&lt;/code&gt; to &lt;code&gt;On&lt;/code&gt; in a staging environment after the common legitimate flows run without unexplained matches. Repeat the false-positive replay, the neighbouring-field control and the application's critical user journeys.&lt;/p&gt;
&lt;p&gt;Enable blocking for a limited production audience or hostname when the deployment allows it. Watch WAF responses, upstream errors and business transactions together. A clean audit log has little value when checkout or login has stopped working.&lt;/p&gt;
&lt;p&gt;Document every exception beside the configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;who approved it;&lt;/li&gt;
&lt;li&gt;the affected hostname, method and route;&lt;/li&gt;
&lt;li&gt;the rule ID or tag;&lt;/li&gt;
&lt;li&gt;the excluded field;&lt;/li&gt;
&lt;li&gt;the request that reproduced the match;&lt;/li&gt;
&lt;li&gt;the controls that still inspect or validate the field;&lt;/li&gt;
&lt;li&gt;the date and CRS version tested.&lt;/li&gt;
&lt;/ul&gt;</content><category term="Application Security"></category><category term="Coraza"></category><category term="Caddy"></category><category term="OWASP CRS"></category><category term="WAF"></category><category term="Application Security"></category><category term="False Positives"></category><category term="WAF Tuning"></category></entry><entry><title>How to Tune Your WAF Without Turning It Off</title><link href="https://www.peakhour.io/blog/how-to-tune-your-waf/" rel="alternate"></link><published>2026-07-28T10:30:00+10:00</published><updated>2026-07-28T10:30:00+10:00</updated><author><name>AC</name></author><id>tag:www.peakhour.io,2026-07-28:/blog/how-to-tune-your-waf/</id><summary type="html">&lt;p&gt;A practical process for running a CRS-based or managed WAF in detection mode, finding repeatable false positives and writing narrow exceptions that survive real application traffic.&lt;/p&gt;</summary><content type="html">&lt;p&gt;The OWASP Core Rule Set has never seen your application.&lt;/p&gt;
&lt;p&gt;Generic patterns let it recognise common signs of SQL injection, cross-site scripting and protocol attacks. Tuning teaches it that your application stores a search expression in JSON, accepts links in a message field or receives an analytics cookie full of punctuation.&lt;/p&gt;
&lt;p&gt;So a new deployment will be noisy.&lt;/p&gt;
&lt;p&gt;Faced with that noise, teams often leave the WAF in log-only mode forever or disable each rule that causes a complaint. Teams need a repeatable route from noisy alerts to safe blocking.&lt;/p&gt;
&lt;p&gt;The work is repetitive and fairly unglamorous: observe real traffic, group related matches, understand the application field involved, write the narrowest safe exception and repeat until legitimate traffic no longer triggers unexplained alerts.&lt;/p&gt;
&lt;h2&gt;Start in detection-only mode&lt;/h2&gt;
&lt;p&gt;Introduce the full OWASP CRS to an existing production application in detection-only mode.&lt;/p&gt;
&lt;p&gt;On ModSecurity or Coraza, set &lt;code&gt;SecRuleEngine DetectionOnly&lt;/code&gt;. The engine records what the rules would have denied, dropped or redirected while allowing the requests through.&lt;/p&gt;
&lt;p&gt;Both engines support this rollout. The &lt;a href="https://github.com/owasp-modsecurity/ModSecurity/wiki/ModSecurity-Frequently-Asked-Questions-%28FAQ%29"&gt;ModSecurity FAQ recommends detection-only mode for a new ruleset&lt;/a&gt;, and &lt;a href="https://www.coraza.io/docs/seclang/directives/#secruleengine"&gt;Coraza implements the same setting&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Run it long enough to see the application's real shape. Allow at least a full week, and often one to two weeks, so the sample includes business days, a weekend and any less frequent jobs, imports or campaigns.&lt;/p&gt;
&lt;p&gt;Exercise authenticated actions, checkout, password reset, messaging, file upload, API writes and administrative tools manually because ordinary browsing may never reach them.&lt;/p&gt;
&lt;p&gt;Detection mode records rule matches while passing every request through, including attacks. Decide in advance what needs to be true before you enable blocking.&lt;/p&gt;
&lt;h2&gt;What changes between WAF products&lt;/h2&gt;
&lt;p&gt;The examples below use SecLang and OWASP CRS because they expose the rule ID, attack class and request field that caused a match. Managed WAFs record much the same information under different names, with varying precision for exceptions.&lt;/p&gt;
&lt;p&gt;Coraza also runs CRS 4. It is a Go implementation of SecLang, and the CRS project recommends it for new CRS 4 deployments. The &lt;a href="/blog/how-to-tune-coraza-waf/"&gt;Coraza tuning guide&lt;/a&gt; covers its audit logging, connectors and runtime exclusions in more detail.&lt;/p&gt;
&lt;p&gt;Body processors and chained rules can behave differently between engines and web-server connectors. Replay the requests that caused the false positive through the engine and connector that handle production traffic.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tuning job&lt;/th&gt;
&lt;th&gt;SecLang / CRS&lt;/th&gt;
&lt;th&gt;Managed WAF examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Observe without blocking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SecRuleEngine DetectionOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Azure Detection mode, AWS rule-action override to Count, Google Cloud Armor preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identify the detection&lt;/td&gt;
&lt;td&gt;numeric rule ID and message&lt;/td&gt;
&lt;td&gt;managed rule ID or name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identify the class&lt;/td&gt;
&lt;td&gt;CRS tag such as &lt;code&gt;attack-sqli&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rule group, category or emitted label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identify the input&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REQUEST_COOKIES:name&lt;/code&gt;, &lt;code&gt;ARGS:field&lt;/code&gt;, header or body target&lt;/td&gt;
&lt;td&gt;matched request component or exclusion selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrow the exception&lt;/td&gt;
&lt;td&gt;target removal by ID or tag, optionally limited to a route&lt;/td&gt;
&lt;td&gt;field exclusion, per-rule exception, label-based follow-on rule or rule skip limited by request conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last row is where the products differ most. ModSecurity and Coraza can stop one rule from inspecting one field on one route. Some managed WAFs can only skip the whole rule for requests matching that route. If that is the smallest exception available, write down exactly what the WAF will stop inspecting and decide whether the route is safe without that rule.&lt;/p&gt;
&lt;h2&gt;Group alerts before reviewing them&lt;/h2&gt;
&lt;p&gt;Reviewing WAF alerts one at a time hides repeated false positives.&lt;/p&gt;
&lt;p&gt;The same harmless field may trigger the same rule across thousands of requests. Conversely, two alerts with the same rule ID may come from completely different application behaviours and need different decisions.&lt;/p&gt;
&lt;p&gt;Group events using at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hostname and URL;&lt;/li&gt;
&lt;li&gt;HTTP method;&lt;/li&gt;
&lt;li&gt;CRS rule ID;&lt;/li&gt;
&lt;li&gt;rule class or attack tag, such as &lt;code&gt;attack-sqli&lt;/code&gt; or &lt;code&gt;attack-xss&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;matched-data variable, including its collection and exact name;&lt;/li&gt;
&lt;li&gt;WAF action and response status;&lt;/li&gt;
&lt;li&gt;event count, distinct clients and spread over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, &lt;code&gt;REQUEST_COOKIES:analytics_id&lt;/code&gt; is much more useful than “SQL injection rule fired”. So is &lt;code&gt;ARGS:message&lt;/code&gt; on &lt;code&gt;POST /api/messages&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The rule ID tells you which detection fired. The attack class tells you which protection an exception would weaken. The matched variable identifies the request field that caused it. The URL and method show where the application uses that field.&lt;/p&gt;
&lt;p&gt;Keep a few examples of the matched values long enough to investigate them, but handle them carefully. Request fields can contain credentials, personal information and customer content. The exception record usually needs the variable name and an explanation, not a permanent copy of the raw value.&lt;/p&gt;
&lt;h2&gt;Expect false positives in a few places&lt;/h2&gt;
&lt;p&gt;A few kinds of input generate most legitimate matches.&lt;/p&gt;
&lt;h3&gt;Request cookies&lt;/h3&gt;
&lt;p&gt;Marketing, attribution, experimentation and client-state cookies often contain encoded or structured values. A generic SQLi, PHP or local-file-inclusion rule may find a suspicious fragment even though the application never treats that cookie as executable input.&lt;/p&gt;
&lt;p&gt;Keep inspection active for the &lt;code&gt;REQUEST_COOKIES&lt;/code&gt; collection and exclude only the cookie that caused the false positive.&lt;/p&gt;
&lt;p&gt;Identify the cookie name, the rules or attack classes it triggers, the routes where it appears, how often it occurs and whether those requests complete normally. If &lt;code&gt;REQUEST_COOKIES:analytics_id&lt;/code&gt; repeatedly triggers the same attack class for legitimate requests, consider an exception for that cookie alone.&lt;/p&gt;
&lt;p&gt;Cookie names need separate treatment. A rule matching &lt;code&gt;REQUEST_COOKIES_NAMES&lt;/code&gt; inspects the names. Capture the exact name safely before writing the exclusion.&lt;/p&gt;
&lt;h3&gt;Unparsed or malformed request bodies&lt;/h3&gt;
&lt;p&gt;Parser errors deserve their own investigation.&lt;/p&gt;
&lt;p&gt;When a selected body processor cannot parse a body, ModSecurity and Coraza set &lt;code&gt;REQBODY_ERROR&lt;/code&gt;. Record the &lt;code&gt;Content-Type&lt;/code&gt; and observed &lt;code&gt;REQBODY_PROCESSOR&lt;/code&gt; because JSON and XML processor selection varies by engine, connector and version. Confirm that a processor ran before relying on the error flag.&lt;/p&gt;
&lt;p&gt;A body that exceeds the configured inspection limit is a different case. It sets &lt;code&gt;INBOUND_DATA_ERROR&lt;/code&gt; and may be inspected only partially. In either case, part of the body may go uninspected, and an attacker can deliberately probe that behaviour.&lt;/p&gt;
&lt;p&gt;Ask the developer who owns the endpoint what it is meant to accept. Fix incorrect content types and body-processor configuration, reproduce the request, then check whether the application and WAF parse it the same way.&lt;/p&gt;
&lt;p&gt;Keep request-body inspection active for the application. Limit any parser exception to the host, URL, method and body behaviour you investigated.&lt;/p&gt;
&lt;h3&gt;Free-text fields&lt;/h3&gt;
&lt;p&gt;Messages, descriptions, comments, support tickets and search boxes are expected to contain language that resembles attack syntax. A customer can discuss SQL without attempting SQL injection. A marketplace listing can contain shell characters. A support message can include a URL.&lt;/p&gt;
&lt;p&gt;Exclude the named field on the affected route and keep inspection active for the rest of the request body.&lt;/p&gt;
&lt;p&gt;For example, the SecLang rule below removes &lt;code&gt;ARGS:message&lt;/code&gt; from every rule tagged &lt;code&gt;attack-sqli&lt;/code&gt; on the messaging endpoint. That is appropriate only if you have decided that SQLi inspection is too noisy for this field across the whole class. Every other argument still receives SQLi inspection, and scam, malicious-link or other attack classes can continue inspecting the message.&lt;/p&gt;
&lt;p&gt;Bring in the developer who owns the endpoint. They can confirm whether the field is genuinely free text, where it is stored or rendered, what server-side validation exists and which security checks still need to see it.&lt;/p&gt;
&lt;h3&gt;Application-specific structured fields&lt;/h3&gt;
&lt;p&gt;Some applications accept query languages, filters, templates, product metadata or nested JSON that naturally looks like code. The syntax may be legitimate, but these fields still deserve careful review before they are excluded.&lt;/p&gt;
&lt;p&gt;Confirm the schema and business use before creating an exception. Check that &lt;code&gt;MATCHED_VAR_NAME&lt;/code&gt; identifies one field, then use that exact named target. Nested JSON naming varies between engines and integrations. Keep SQLi inspection active for the rest of the API when one structured field needs an exception.&lt;/p&gt;
&lt;h2&gt;Make the smallest exception that works&lt;/h2&gt;
&lt;p&gt;Work down this list and stop as soon as the known false positive is fixed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;host, exact URL, method, rule ID or attack class, and exact named variable;&lt;/li&gt;
&lt;li&gt;host, reviewed URL family, method, class, and named variable;&lt;/li&gt;
&lt;li&gt;host, exact URL and rule ID;&lt;/li&gt;
&lt;li&gt;host, rule ID;&lt;/li&gt;
&lt;li&gt;global named-variable exclusion.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Start with the first option. Move further down only when the logs and the application design justify a wider exception.&lt;/p&gt;
&lt;p&gt;A global exception can make sense for a named cookie or header that has no security meaning anywhere in the application. Before adding one, make sure the false positive appears across several routes and clients, and that those requests complete normally. Ask the development team to confirm that no backend interprets the field.&lt;/p&gt;
&lt;p&gt;Even then, remove only that named field from the relevant rule or attack class. An exception for &lt;code&gt;REQUEST_COOKIES:analytics_id&lt;/code&gt; removes one known input from SQLi inspection. An exception for the whole cookie collection removes every cookie.&lt;/p&gt;
&lt;p&gt;Reserve global exceptions for logs that identify the matched field. Unknown body fields, unresolved cookie names and parser failures need manual investigation.&lt;/p&gt;
&lt;h2&gt;Keep your exceptions outside CRS&lt;/h2&gt;
&lt;p&gt;Put application-specific exceptions in local configuration and leave the Core Rule Set files unchanged. This keeps CRS upgrades separate from changes made for your application.&lt;/p&gt;
&lt;p&gt;To stop one rule inspecting a named field everywhere, ModSecurity and Coraza support:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;SecRuleUpdateTargetById&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;942100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!REQUEST_COOKIES:analytics_id&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Load this directive after the CRS rules because the engine must load a rule before updating it.&lt;/p&gt;
&lt;p&gt;For a route-specific exception, use a runtime control before the CRS rule executes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# Load before CRS, normally from REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.&lt;/span&gt;
&lt;span class="c"&gt;# Use this rule only for api.example.com.&lt;/span&gt;
&lt;span class="c"&gt;# Confirm that local rule ID 10001 is unused.&lt;/span&gt;
&lt;span class="nb"&gt;SecRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;REQUEST_FILENAME&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;@streq /api/messages&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;id:10001,phase:1,pass,nolog,t:none,chain&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;SecRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;REQUEST_METHOD&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;@streq POST&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;t:none,ctl:ruleRemoveTargetByTag=attack-sqli;ARGS:message&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this exception, SQLi rules stop reading &lt;code&gt;ARGS:message&lt;/code&gt; only on &lt;code&gt;POST /api/messages&lt;/code&gt;. They still inspect other fields and routes, while other attack classes can continue inspecting the message.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://coreruleset.org/docs/2-how-crs-works/2-3-false-positives-and-tuning/"&gt;CRS false-positive tuning guide&lt;/a&gt; documents configure-time and runtime exclusions by rule ID or tag. The &lt;code&gt;attack-sqli&lt;/code&gt; selector above is a literal tag that works with both ModSecurity and Coraza. ModSecurity runtime target selectors accept literal collection keys. &lt;a href="https://www.coraza.io/docs/seclang/actions/#ctl"&gt;Coraza also accepts regular expressions for collection keys&lt;/a&gt;, using &lt;code&gt;/pattern/&lt;/code&gt; syntax.&lt;/p&gt;
&lt;p&gt;A target exclusion may affect only the first rule in a chained CRS detection. Test the exact rules and requests on your deployed engine before relying on the exception.&lt;/p&gt;
&lt;p&gt;Use rule IDs or exact attack-class tags. Rule messages can change and may contain request data.&lt;/p&gt;
&lt;h2&gt;Managed WAFs expose different controls&lt;/h2&gt;
&lt;p&gt;Each managed provider expresses the exception differently.&lt;/p&gt;
&lt;p&gt;AWS Managed Rules can emit a label while an offending rule runs in &lt;code&gt;Count&lt;/code&gt; mode. A later custom rule can block that label everywhere except the route that produced the false positive. The exception affects enforcement for the whole managed rule on that route. &lt;a href="/blog/how-to-tune-aws-waf/"&gt;How to Tune AWS WAF&lt;/a&gt; follows that evaluation path from the first counted request to restored blocking.&lt;/p&gt;
&lt;p&gt;Azure Application Gateway WAF can exclude a named cookie, header or argument from a particular managed rule. Its request exceptions can instead skip a selected rule for matching traffic, and that feature is currently in preview. A custom &lt;code&gt;Allow&lt;/code&gt; rule bypasses the managed CRS, DRS and bot rules for the request. &lt;a href="/blog/how-to-tune-azure-application-gateway-waf/"&gt;How to Tune Azure Application Gateway WAF&lt;/a&gt; shows how to choose among those controls.&lt;/p&gt;
&lt;p&gt;Cloudflare exceptions skip a selected managed rule for the whole matching request. Security Events may show rule 949110 as the blocking event because it checks the final anomaly score; Additional logs identify the rules that added that score. &lt;a href="/blog/how-to-tune-cloudflare-waf/"&gt;How to Tune Cloudflare WAF&lt;/a&gt; covers that investigation and the ordering of account-level and zone-level exceptions.&lt;/p&gt;
&lt;p&gt;Google Cloud Armor can exclude a named request field from a versioned OWASP signature. Route-specific tuning also depends on the security-policy expression and rule priority, because a general WAF rule may inspect the same request later. &lt;a href="/blog/how-to-tune-google-cloud-armor-waf/"&gt;How to Tune Google Cloud Armor&lt;/a&gt; covers preview, signature selection and field exclusions.&lt;/p&gt;
&lt;p&gt;F5 BIG-IP Advanced WAF separates policy enforcement mode, violation actions and signature or entity staging. Its overrides can target a URL parameter, cookie, content profile, URL or the whole policy. &lt;a href="/blog/how-to-tune-f5-big-ip-advanced-waf/"&gt;How to Tune F5 BIG-IP Advanced WAF&lt;/a&gt; follows those controls from Transparent mode to tested blocking.&lt;/p&gt;
&lt;p&gt;Provider interfaces, ruleset versions and preview features change. Record the rule name or versioned signature, the field or request being exempted and what the WAF will still inspect. Test the requests that led to the exception before enabling it.&lt;/p&gt;
&lt;h2&gt;Iterate until the false positives are gone&lt;/h2&gt;
&lt;p&gt;After adding an exception, group the alerts again. Check whether that false-positive pattern disappeared, whether a related rule started matching the same field and whether another route was affected. Look for legitimate flows that the first observation window missed.&lt;/p&gt;
&lt;p&gt;Repeat the cycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;collect detection-only traffic that covers normal application use;&lt;/li&gt;
&lt;li&gt;group by route, method, rule ID, rule class and named variable;&lt;/li&gt;
&lt;li&gt;classify abusive traffic separately from legitimate matches;&lt;/li&gt;
&lt;li&gt;create or narrow exceptions;&lt;/li&gt;
&lt;li&gt;rerun automated and manual application flows;&lt;/li&gt;
&lt;li&gt;collect another window.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Aim for zero known false positives in the traffic you observed and the critical flows you tested. A new endpoint, cookie, client or release can produce fresh false positives tomorrow.&lt;/p&gt;
&lt;p&gt;Use a higher anomaly threshold only during tuning because it can allow attacks that the normal threshold would have blocked. Write an exception for the rule and field causing the false positive, then return to the intended threshold.&lt;/p&gt;
&lt;h2&gt;Document every exception&lt;/h2&gt;
&lt;p&gt;Give every exception an owner, a reason and a review date.&lt;/p&gt;
&lt;p&gt;Record:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hostname, URL or URL family, and HTTP method;&lt;/li&gt;
&lt;li&gt;CRS version, rule IDs and attack-class tags;&lt;/li&gt;
&lt;li&gt;matched collection and exact variable name;&lt;/li&gt;
&lt;li&gt;sample event IDs and observation window;&lt;/li&gt;
&lt;li&gt;why the traffic is legitimate;&lt;/li&gt;
&lt;li&gt;which customer or application flow depends on it;&lt;/li&gt;
&lt;li&gt;what protection is removed and what remains active;&lt;/li&gt;
&lt;li&gt;developer and security approvers;&lt;/li&gt;
&lt;li&gt;date introduced and next review date;&lt;/li&gt;
&lt;li&gt;the release, incident or ticket associated with the change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep the exception under version control. Give it a description that another engineer can understand without finding the original chat thread. Review it after CRS upgrades because rule IDs, paranoia levels and rule behaviour can change. The CRS project specifically recommends returning to detection mode long enough to exercise normal traffic after a major migration.&lt;/p&gt;
&lt;p&gt;Remove an exception when the application no longer needs it. If the application changes its cookie, parser or validation, restore inspection.&lt;/p&gt;
&lt;h2&gt;Decide what a WAF hit should do&lt;/h2&gt;
&lt;p&gt;Security and application owners must decide which matches to log, block, rate limit, challenge or review.&lt;/p&gt;
&lt;p&gt;With CRS anomaly scoring, an individual rule normally adds to a transaction score. The WAF blocks when the inbound score crosses the configured threshold, allowing several weak signals to add up before a customer is blocked.&lt;/p&gt;
&lt;p&gt;Decide which outcomes you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Log:&lt;/strong&gt; useful during tuning and for low-confidence signals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block:&lt;/strong&gt; appropriate when several signals point to an attack and the affected flow has been tested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limit:&lt;/strong&gt; useful for repeated behaviour across several requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Challenge or step up:&lt;/strong&gt; where the product supports it, useful for browser and account flows where a hard block would be unnecessarily disruptive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review:&lt;/strong&gt; appropriate for high-value actions where context matters more than a generic signature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Respond to a single SQLi alert at the request level. Shared networks, mobile carriers and proxies make IP identity unreliable. Repeated WAF hits on the same route can inform a rate limit, but the limit should use the best available client, session or account key and have a clear window.&lt;/p&gt;
&lt;p&gt;Track WAF blocks and rate limits separately. A ModSecurity or Coraza block commonly returns &lt;code&gt;403&lt;/code&gt;; a rate limit commonly returns &lt;code&gt;429&lt;/code&gt;. If the logs say the WAF blocked a request but the origin returned success, find out why before trusting the enforcement reports.&lt;/p&gt;
&lt;h2&gt;Recheck the WAF after application releases&lt;/h2&gt;
&lt;p&gt;A release may add an endpoint, rename a field, introduce GraphQL, change JSON shape, add a marketing cookie or move validation between services. Any of those changes can make an old exception too broad or cause a new legitimate field to trigger CRS.&lt;/p&gt;
&lt;p&gt;Include the development team in the release process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;list new or changed public routes and methods;&lt;/li&gt;
&lt;li&gt;identify free-text, upload and structured fields;&lt;/li&gt;
&lt;li&gt;run staging traffic through the same CRS configuration;&lt;/li&gt;
&lt;li&gt;test authenticated and high-value journeys;&lt;/li&gt;
&lt;li&gt;watch the grouped detections during the production rollout;&lt;/li&gt;
&lt;li&gt;give new or substantially changed routes an observation period before enforcing new rules on them;&lt;/li&gt;
&lt;li&gt;review whether old exceptions can be removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For major CRS upgrades, treat the ruleset itself as a release. A ruleset upgrade can change rule IDs and anomaly-score behaviour while the application stays the same.&lt;/p&gt;
&lt;h2&gt;Know when tuning is complete&lt;/h2&gt;
&lt;p&gt;A traditional WAF is ready to enforce when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;normal production traffic, including the less common application flows, has been reviewed;&lt;/li&gt;
&lt;li&gt;critical application flows have been tested with developers;&lt;/li&gt;
&lt;li&gt;every high-volume legitimate match has a documented, narrow exception;&lt;/li&gt;
&lt;li&gt;parser failures and unresolved matched variables have named owners and specific resolutions;&lt;/li&gt;
&lt;li&gt;WAF blocks produce the expected response;&lt;/li&gt;
&lt;li&gt;rate limiting is handled separately from payload detection;&lt;/li&gt;
&lt;li&gt;exceptions are versioned, approved and reviewable;&lt;/li&gt;
&lt;li&gt;new rules and releases can be observed before they start blocking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once those checks pass, enable blocking in stages and keep watching the same groups of alerts. Revisit the exceptions whenever the application or ruleset changes.&lt;/p&gt;
&lt;p&gt;For the broader decision about where a WAF helps and where it does not, see &lt;a href="/blog/when-and-why-to-use-a-waf/"&gt;When and Why to Use a WAF&lt;/a&gt;. You can also read about Peakhour's &lt;a href="/products/waf/"&gt;web application firewall&lt;/a&gt;.&lt;/p&gt;</content><category term="Application Security"></category><category term="WAF"></category><category term="OWASP CRS"></category><category term="ModSecurity"></category><category term="Coraza"></category><category term="Application Security"></category><category term="False Positives"></category><category term="WAF Tuning"></category></entry></feed>