<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Peakhour.IO - Coraza</title><link href="https://www.peakhour.io/" rel="alternate"></link><link href="https://www.peakhour.io/feeds/tag/coraza.atom.xml" rel="self"></link><id>https://www.peakhour.io/</id><updated>2026-07-29T09:00:00+10:00</updated><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>