From 1af5719b65014dde0e1c9c1b74757b102e9009d5 Mon Sep 17 00:00:00 2001 From: vara Date: Wed, 14 Oct 2026 13:28:11 -0700 Subject: [PATCH 1/5] Document Kibana Core Rule Set exceptions for Gateway WAF (EV-7238) Kibana Discover returns 403 when a WAFPolicy with the Core Rule Set covers the Manager gateway: the Elasticsearch query bodies read as SQL injection and the url argument as SSRF. Add a worked GlobalWAFPlugin example that removes those rules on the Kibana paths, and link it from the false-positive troubleshooting section. --- .../threat/gateway-waf/troubleshooting.mdx | 2 + .../threat/gateway-waf/write-custom-rules.mdx | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index 93661f150c..6d120a962c 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -181,6 +181,8 @@ For how to write exception rules within your namespace's ID range, see [Write cu ::: +When a baseline rule misfires on a known application — for example, Kibana Discover returning `403` because the Core Rule Set reads its Elasticsearch query bodies as SQL injection — tune it with a cluster-scoped `GlobalWAFPlugin`, which may adjust baseline rules. See [Unblock Kibana Discover](./write-custom-rules.mdx#example-unblock-kibana-discover). + --- ## General diagnostic sequence diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 65a86b7a57..7ed211fd53 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -132,6 +132,59 @@ A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecuri Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via [Coraza](https://coraza.io/); the [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. +## Example: unblock Kibana Discover + +Kibana sends an Elasticsearch query in the body of its search requests, and a `url` argument on many requests. With the Core Rule Set enabled, the WAF reads the query as SQL injection and the `url` argument as server-side request forgery, so Kibana Discover fails with HTTP 403. The Core Rule Set is working as designed — Kibana needs app-specific exceptions, the same way the Core Rule Set ships exceptions for applications such as phpMyAdmin and WordPress. + +These exceptions remove inspection for Core Rule Set baseline rules, so they belong in a `GlobalWAFPlugin`. A namespaced `WAFPlugin` cannot alter a baseline rule — the admission webhook rejects it with `ProtectedIDRemoval`. A `GlobalWAFPlugin` is cluster-scoped and may tune baseline rules. + +These rules fire for Kibana (OWASP Core Rule Set 4.14.0, paranoia level 1): + +| Path | Rule | Reads as | +| :--- | :--- | :--- | +| `/tigera-kibana/internal/...` search requests | `942100` | SQL injection in the request-body arguments | +| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` | Server-side request forgery in the `url` argument | + +Rule `949110` also appears. It adds the scores from the rules above and blocks once the total crosses the threshold. Remove the rules above and the score stays under the threshold, so `949110` stops firing on its own — do not exclude it. + +Apply the plugin and reference it from the `default` `GlobalWAFPolicy`, which every `WAFPolicy` reads: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: kibana-exceptions +spec: + description: "Core Rule Set exceptions for Kibana on the Manager gateway" + before: | + # Elasticsearch query bodies on the search paths read as SQL injection. + SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/internal/" \ + "id:100000001,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ + ctl:ruleRemoveTargetById=942100;ARGS" + # The url argument reads as server-side request forgery. + SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ + "id:100000002,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ + ctl:ruleRemoveTargetById=931100;ARGS:url,\ + ctl:ruleRemoveTargetById=934110;ARGS:url" +--- +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + plugins: + - kind: GlobalWAFPlugin + name: kibana-exceptions +``` + +If the cluster already has a `default` `GlobalWAFPolicy`, add the plugin to its existing `plugins` list rather than replacing the object. + +:::note + +The rule IDs `100000001` and `100000002` sit above every reserved range. The search paths differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm which paths and rules fire, and adjust the `@beginsWith` prefixes to match. + +::: + ## Next steps - [Validate configuration with Rego](./validate-config.mdx) — enforce guardrails on the merged ruleset. From 50969aab549931dabb1f2b924cf8ef6377904633 Mon Sep 17 00:00:00 2001 From: vara Date: Wed, 14 Oct 2026 14:09:24 -0700 Subject: [PATCH 2/5] Correct the Kibana WAF exception after cluster validation ruleRemoveTargetById cannot subtract the parsed search-body arguments, so remove rule 942100 scoped to the bsearch path instead; keep the url exception as a surgical target removal. Validated on a live gateway: Discover passes while SQLi and SSRF stay blocked on other paths. Also document the cluster-wide scope of the global plugin. --- .../threat/gateway-waf/write-custom-rules.mdx | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 7ed211fd53..8b8da40c7c 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -136,16 +136,18 @@ Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via Kibana sends an Elasticsearch query in the body of its search requests, and a `url` argument on many requests. With the Core Rule Set enabled, the WAF reads the query as SQL injection and the `url` argument as server-side request forgery, so Kibana Discover fails with HTTP 403. The Core Rule Set is working as designed — Kibana needs app-specific exceptions, the same way the Core Rule Set ships exceptions for applications such as phpMyAdmin and WordPress. -These exceptions remove inspection for Core Rule Set baseline rules, so they belong in a `GlobalWAFPlugin`. A namespaced `WAFPlugin` cannot alter a baseline rule — the admission webhook rejects it with `ProtectedIDRemoval`. A `GlobalWAFPlugin` is cluster-scoped and may tune baseline rules. +These exceptions adjust Core Rule Set baseline rules, so they belong in a `GlobalWAFPlugin`. A namespaced `WAFPlugin` cannot alter a baseline rule — the admission webhook rejects it with `ProtectedIDRemoval` — and a namespaced `WAFPolicy` cannot reference a `GlobalWAFPlugin`. A cluster-scoped `GlobalWAFPlugin`, loaded through the `default` `GlobalWAFPolicy`, is the only configuration that both tunes a baseline rule and reaches the gateway. These rules fire for Kibana (OWASP Core Rule Set 4.14.0, paranoia level 1): -| Path | Rule | Reads as | -| :--- | :--- | :--- | -| `/tigera-kibana/internal/...` search requests | `942100` | SQL injection in the request-body arguments | -| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` | Server-side request forgery in the `url` argument | +| Path | Rule | Reads as | Exception | +| :--- | :--- | :--- | :--- | +| `/tigera-kibana/internal/bsearch` (Discover search) | `942100` | SQL injection in the search body | Remove rule `942100` on that path only | +| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` | Server-side request forgery in the `url` argument | Remove the `url` argument from those two rules | -Rule `949110` also appears. It adds the scores from the rules above and blocks once the total crosses the threshold. Remove the rules above and the score stays under the threshold, so `949110` stops firing on its own — do not exclude it. +Rule `949110` also appears. It adds the scores from the rules above and blocks once the total crosses the threshold. Remove the matches above and the score stays under the threshold, so `949110` stops firing on its own — do not exclude it. + +Scope each exception as narrowly as it can go. The search body is parsed into arguments with dynamic names, so the exception removes rule `942100` for the one search path — keep the path match on the exact endpoint, so the rule still inspects every other request. The `url` argument has a fixed name, so its exception removes only that argument from the two forgery rules and leaves the rest of each rule in force. Apply the plugin and reference it from the `default` `GlobalWAFPolicy`, which every `WAFPolicy` reads: @@ -157,11 +159,13 @@ metadata: spec: description: "Core Rule Set exceptions for Kibana on the Manager gateway" before: | - # Elasticsearch query bodies on the search paths read as SQL injection. - SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/internal/" \ + # Discover sends the Elasticsearch query in the request body, which rule + # 942100 reads as SQL injection. Remove that rule for the search path only. + SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/internal/bsearch" \ "id:100000001,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ - ctl:ruleRemoveTargetById=942100;ARGS" - # The url argument reads as server-side request forgery. + ctl:ruleRemoveById=942100" + # The url argument reads as server-side request forgery. Remove only that + # argument from the two forgery rules. SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ "id:100000002,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ ctl:ruleRemoveTargetById=931100;ARGS:url,\ @@ -181,7 +185,13 @@ If the cluster already has a `default` `GlobalWAFPolicy`, add the plugin to its :::note -The rule IDs `100000001` and `100000002` sit above every reserved range. The search paths differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm which paths and rules fire, and adjust the `@beginsWith` prefixes to match. +The plugin is cluster-scoped: through the `default` `GlobalWAFPolicy` it reaches every `WAFPolicy`. The `/tigera-kibana/` path match keeps it inert on gateways that don't serve Kibana, but a second gateway serving the same paths receives the same exceptions. This wide reach is unavoidable — only a `GlobalWAFPlugin` may adjust a baseline rule — so keep every match scoped to the Kibana paths. + +::: + +:::note + +The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint differs between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm which paths and rules fire, and add a path match per search endpoint rather than widening the prefix. ::: From f67e004eed05df26222ee92f045bbf7390d39b92 Mon Sep 17 00:00:00 2001 From: vara Date: Fri, 16 Oct 2026 11:41:36 -0700 Subject: [PATCH 3/5] Fix the Kibana WAF exception: skip body inspection, not rule 942100 Removing rule 942100 does not unblock real Kibana Discover. A Discover search sends an arbitrary Elasticsearch query in the body; the Core Rule Set scores it across many rules and blocks on the 949110 anomaly aggregator, and the matching rules change from query to query. Skip request-body inspection on the search path instead, which keeps URL, header, and method inspection in force and stays correct across queries. --- .../threat/gateway-waf/troubleshooting.mdx | 2 +- .../threat/gateway-waf/write-custom-rules.mdx | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index 6d120a962c..e93397a462 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -181,7 +181,7 @@ For how to write exception rules within your namespace's ID range, see [Write cu ::: -When a baseline rule misfires on a known application — for example, Kibana Discover returning `403` because the Core Rule Set reads its Elasticsearch query bodies as SQL injection — tune it with a cluster-scoped `GlobalWAFPlugin`, which may adjust baseline rules. See [Unblock Kibana Discover](./write-custom-rules.mdx#example-unblock-kibana-discover). +When the Core Rule Set misfires on a known application — for example, Kibana Discover returning `403` because the Core Rule Set reads its Elasticsearch query bodies as attacks — tune it with a cluster-scoped `GlobalWAFPlugin`, which may adjust how the Core Rule Set runs. See [Unblock Kibana Discover](./write-custom-rules.mdx#example-unblock-kibana-discover). --- diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 8b8da40c7c..78baaa2c25 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -134,20 +134,18 @@ Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via ## Example: unblock Kibana Discover -Kibana sends an Elasticsearch query in the body of its search requests, and a `url` argument on many requests. With the Core Rule Set enabled, the WAF reads the query as SQL injection and the `url` argument as server-side request forgery, so Kibana Discover fails with HTTP 403. The Core Rule Set is working as designed — Kibana needs app-specific exceptions, the same way the Core Rule Set ships exceptions for applications such as phpMyAdmin and WordPress. +Kibana sends an Elasticsearch query in the body of its search requests, and a `url` argument on many requests. With the Core Rule Set enabled, the WAF reads the search body as an attack and the `url` argument as server-side request forgery, so Kibana Discover fails with HTTP 403. The Core Rule Set is working as designed — Kibana needs app-specific exceptions, the same way the Core Rule Set ships exceptions for applications such as phpMyAdmin and WordPress. -These exceptions adjust Core Rule Set baseline rules, so they belong in a `GlobalWAFPlugin`. A namespaced `WAFPlugin` cannot alter a baseline rule — the admission webhook rejects it with `ProtectedIDRemoval` — and a namespaced `WAFPolicy` cannot reference a `GlobalWAFPlugin`. A cluster-scoped `GlobalWAFPlugin`, loaded through the `default` `GlobalWAFPolicy`, is the only configuration that both tunes a baseline rule and reaches the gateway. +These exceptions change how the Core Rule Set runs, so they belong in a `GlobalWAFPlugin`. A namespaced `WAFPlugin` cannot alter a baseline rule — the admission webhook rejects it with `ProtectedIDRemoval` — and a namespaced `WAFPolicy` cannot reference a `GlobalWAFPlugin`. A cluster-scoped `GlobalWAFPlugin`, loaded through the `default` `GlobalWAFPolicy`, is the only configuration that both tunes the Core Rule Set and reaches the gateway. -These rules fire for Kibana (OWASP Core Rule Set 4.14.0, paranoia level 1): +A Discover search carries a full Elasticsearch query in its body — sort fields, highlight tags, field lists, nested filters, and large numbers. The Core Rule Set cannot tell this query apart from an attack: many of its rules each match some part of the body and add to an anomaly score, and rule `949110` blocks the request once that score crosses the threshold. The set of rules that match changes from query to query, because each search sends different fields and values. Removing one rule, or even the whole SQL-injection group, does not reliably unblock Discover — another rule brings the score back over the threshold on the next query. -| Path | Rule | Reads as | Exception | -| :--- | :--- | :--- | :--- | -| `/tigera-kibana/internal/bsearch` (Discover search) | `942100` | SQL injection in the search body | Remove rule `942100` on that path only | -| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` | Server-side request forgery in the `url` argument | Remove the `url` argument from those two rules | +So the exception skips Core Rule Set inspection of the request **body** for the one search path, instead of removing named rules. The WAF still inspects the URL, headers, and method on that path, and still inspects the body on every other path. The `url` argument is different: it rides in the URL, has a fixed name, and trips two specific rules, so its exception removes just that argument from those two rules. -Rule `949110` also appears. It adds the scores from the rules above and blocks once the total crosses the threshold. Remove the matches above and the score stays under the threshold, so `949110` stops firing on its own — do not exclude it. - -Scope each exception as narrowly as it can go. The search body is parsed into arguments with dynamic names, so the exception removes rule `942100` for the one search path — keep the path match on the exact endpoint, so the rule still inspects every other request. The `url` argument has a fixed name, so its exception removes only that argument from the two forgery rules and leaves the rest of each rule in force. +| Path | What fires | Exception | +| :--- | :--- | :--- | +| `/tigera-kibana/internal/bsearch` (Discover search) | The search body scores across several Core Rule Set rules and crosses the anomaly threshold (`949110`) | Skip request-body inspection on that path only | +| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` read the `url` argument as server-side request forgery | Remove the `url` argument from those two rules | Apply the plugin and reference it from the `default` `GlobalWAFPolicy`, which every `WAFPolicy` reads: @@ -159,11 +157,12 @@ metadata: spec: description: "Core Rule Set exceptions for Kibana on the Manager gateway" before: | - # Discover sends the Elasticsearch query in the request body, which rule - # 942100 reads as SQL injection. Remove that rule for the search path only. + # Discover sends an arbitrary Elasticsearch query in the request body, which + # the Core Rule Set scores as an attack. Skip request-body inspection for the + # search path only; URL, header, and method inspection stay in force. SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/internal/bsearch" \ "id:100000001,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ - ctl:ruleRemoveById=942100" + ctl:requestBodyAccess=Off" # The url argument reads as server-side request forgery. Remove only that # argument from the two forgery rules. SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ @@ -191,7 +190,7 @@ The plugin is cluster-scoped: through the `default` `GlobalWAFPolicy` it reaches :::note -The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint differs between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm which paths and rules fire, and add a path match per search endpoint rather than widening the prefix. +The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint can differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm the search path, and add a path match per search endpoint rather than widening the prefix. Because the body exception keys on the path and not on named rules, it keeps working as the Core Rule Set changes which rules match. ::: From 4f7017ba16e390cd78038629133878396e14854c Mon Sep 17 00:00:00 2001 From: vara Date: Fri, 16 Oct 2026 15:39:59 -0700 Subject: [PATCH 4/5] Harden the Kibana WAF exception per review Scope the bsearch body exception as narrowly as possible: exact path match (not a prefix), POST only, and pinned to the Manager gateway host, so it never applies on another gateway. Pin the url-argument exception to the same host. Add a trade-off note that bsearch bodies are not inspected on that host. --- .../threat/gateway-waf/write-custom-rules.mdx | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 78baaa2c25..1483325e0a 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -140,12 +140,12 @@ These exceptions change how the Core Rule Set runs, so they belong in a `GlobalW A Discover search carries a full Elasticsearch query in its body — sort fields, highlight tags, field lists, nested filters, and large numbers. The Core Rule Set cannot tell this query apart from an attack: many of its rules each match some part of the body and add to an anomaly score, and rule `949110` blocks the request once that score crosses the threshold. The set of rules that match changes from query to query, because each search sends different fields and values. Removing one rule, or even the whole SQL-injection group, does not reliably unblock Discover — another rule brings the score back over the threshold on the next query. -So the exception skips Core Rule Set inspection of the request **body** for the one search path, instead of removing named rules. The WAF still inspects the URL, headers, and method on that path, and still inspects the body on every other path. The `url` argument is different: it rides in the URL, has a fixed name, and trips two specific rules, so its exception removes just that argument from those two rules. +So the exception skips Core Rule Set inspection of the request **body**, instead of removing named rules — and it matches as narrowly as it can: only a `POST`, to the Manager gateway host, on the exact search path. The WAF still inspects the URL, headers, and method on that request, still inspects the body on every other path, and the exception never applies on another gateway that serves the same path. The `url` argument is different: it rides in the URL, has a fixed name, and trips two specific rules, so its exception removes just that argument from those two rules — pinned to the same host. -| Path | What fires | Exception | +| Request | What fires | Exception | | :--- | :--- | :--- | -| `/tigera-kibana/internal/bsearch` (Discover search) | The search body scores across several Core Rule Set rules and crosses the anomaly threshold (`949110`) | Skip request-body inspection on that path only | -| `/tigera-kibana/...` requests with a `url` argument | `931100`, `934110` read the `url` argument as server-side request forgery | Remove the `url` argument from those two rules | +| `POST /tigera-kibana/internal/bsearch` on the Manager host (Discover search) | The search body scores across several Core Rule Set rules and crosses the anomaly threshold (`949110`) | Skip request-body inspection for that one request | +| `/tigera-kibana/...` requests with a `url` argument, on the Manager host | `931100`, `934110` read the `url` argument as server-side request forgery | Remove the `url` argument from those two rules | Apply the plugin and reference it from the `default` `GlobalWAFPolicy`, which every `WAFPolicy` reads: @@ -157,18 +157,27 @@ metadata: spec: description: "Core Rule Set exceptions for Kibana on the Manager gateway" before: | - # Discover sends an arbitrary Elasticsearch query in the request body, which - # the Core Rule Set scores as an attack. Skip request-body inspection for the - # search path only; URL, header, and method inspection stay in force. - SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/internal/bsearch" \ - "id:100000001,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ - ctl:requestBodyAccess=Off" - # The url argument reads as server-side request forgery. Remove only that - # argument from the two forgery rules. - SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ - "id:100000002,phase:1,pass,nolog,t:none,t:urlDecodeUni,t:normalizePath,\ + # Discover sends its Elasticsearch query in the body of a POST to one search + # path. The Core Rule Set scores that body as an attack, so skip request-body + # inspection for exactly that request: a POST, to the Manager gateway host, on + # the exact search path. URL, header, and method inspection stay in force. + # Replace manager.example.com with your Manager gateway hostname + # (Manager spec.ingressGateway.hostname). + SecRule REQUEST_METHOD "@streq POST" \ + "id:100000001,phase:1,pass,nolog,t:none,\ + ctl:requestBodyAccess=Off,chain" + SecRule REQUEST_HEADERS:Host "@streq manager.example.com" \ + "t:none,t:lowercase,chain" + SecRule REQUEST_FILENAME "@streq /tigera-kibana/internal/bsearch" \ + "t:none,t:urlDecodeUni,t:normalizePath" + # The url argument reads as server-side request forgery. On the Manager host + # only, remove that one argument from the two forgery rules. + SecRule REQUEST_HEADERS:Host "@streq manager.example.com" \ + "id:100000002,phase:1,pass,nolog,t:none,t:lowercase,\ ctl:ruleRemoveTargetById=931100;ARGS:url,\ - ctl:ruleRemoveTargetById=934110;ARGS:url" + ctl:ruleRemoveTargetById=934110;ARGS:url,chain" + SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ + "t:none,t:urlDecodeUni,t:normalizePath" --- apiVersion: applicationlayer.projectcalico.org/v3 kind: GlobalWAFPolicy @@ -182,15 +191,21 @@ spec: If the cluster already has a `default` `GlobalWAFPolicy`, add the plugin to its existing `plugins` list rather than replacing the object. +:::caution + +Trade-off: on the Manager gateway host, the WAF does not inspect the **body** of a Discover search (the `POST` to `/tigera-kibana/internal/bsearch`). Its URL, headers, and method are still inspected, and every other request, path, and host is inspected in full. + +::: + :::note -The plugin is cluster-scoped: through the `default` `GlobalWAFPolicy` it reaches every `WAFPolicy`. The `/tigera-kibana/` path match keeps it inert on gateways that don't serve Kibana, but a second gateway serving the same paths receives the same exceptions. This wide reach is unavoidable — only a `GlobalWAFPlugin` may adjust a baseline rule — so keep every match scoped to the Kibana paths. +The plugin is cluster-scoped — through the `default` `GlobalWAFPolicy` it reaches every `WAFPolicy` — but each rule is pinned to the Manager gateway host with `REQUEST_HEADERS:Host`, so a second gateway serving the same paths is unaffected. Set the host in both rules to your Manager gateway hostname. ::: :::note -The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint can differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm the search path, and add a path match per search endpoint rather than widening the prefix. Because the body exception keys on the path and not on named rules, it keeps working as the Core Rule Set changes which rules match. +The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint can differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm the search path, and add a rule per search endpoint rather than loosening the exact-path match. Because the body exception keys on the request (`POST` + host + path) and not on named rules, it keeps working as the Core Rule Set changes which rules match. ::: From 493cd87123e7ded0ee453b00311d49c99a2ccbc9 Mon Sep 17 00:00:00 2001 From: vara Date: Fri, 16 Oct 2026 15:44:32 -0700 Subject: [PATCH 5/5] Address review feedback on the Kibana WAF example - Pin both rules to the Manager host with SERVER_NAME (from :authority); move the ctl to the last rule of the chain (Coraza runs a starter's ctl even when the chain fails) and drop the id from chained rules. - Replace the re-apply note with a JSON patch that appends to spec.plugins, so an existing default GlobalWAFPolicy is not clobbered. - Reword the scope note: it loads on every route but fires only on the Manager host; document the namespaced Detect-only alternative and its cost. - Explain why the 100,000,000+ rule IDs are collision-safe. - Note in troubleshooting that a GlobalWAFPlugin loads cluster-wide. --- .../threat/gateway-waf/troubleshooting.mdx | 2 +- .../threat/gateway-waf/write-custom-rules.mdx | 47 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index e93397a462..68716a4b0b 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -181,7 +181,7 @@ For how to write exception rules within your namespace's ID range, see [Write cu ::: -When the Core Rule Set misfires on a known application — for example, Kibana Discover returning `403` because the Core Rule Set reads its Elasticsearch query bodies as attacks — tune it with a cluster-scoped `GlobalWAFPlugin`, which may adjust how the Core Rule Set runs. See [Unblock Kibana Discover](./write-custom-rules.mdx#example-unblock-kibana-discover). +When the Core Rule Set misfires on a known application — for example, Kibana Discover returning `403` because the Core Rule Set reads its Elasticsearch query bodies as attacks — tune it with a cluster-scoped `GlobalWAFPlugin`, which may adjust how the Core Rule Set runs. A `GlobalWAFPlugin` loads on every WAF-covered route in the cluster, so pin its rules to the target host and path. See [Unblock Kibana Discover](./write-custom-rules.mdx#example-unblock-kibana-discover). --- diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 1483325e0a..76da799362 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -159,25 +159,27 @@ spec: before: | # Discover sends its Elasticsearch query in the body of a POST to one search # path. The Core Rule Set scores that body as an attack, so skip request-body - # inspection for exactly that request: a POST, to the Manager gateway host, on + # inspection for exactly that request: on the Manager gateway host, a POST to # the exact search path. URL, header, and method inspection stay in force. # Replace manager.example.com with your Manager gateway hostname - # (Manager spec.ingressGateway.hostname). - SecRule REQUEST_METHOD "@streq POST" \ - "id:100000001,phase:1,pass,nolog,t:none,\ - ctl:requestBodyAccess=Off,chain" - SecRule REQUEST_HEADERS:Host "@streq manager.example.com" \ - "t:none,t:lowercase,chain" + # (Manager spec.ingressGateway.hostname). SERVER_NAME is the gateway host from + # the :authority header. The ctl sits on the last rule of the chain, not the + # first, so it only runs when the whole chain matches. The chained rules carry + # no id — the admission check rejects a chained rule that has one. + SecRule SERVER_NAME "@streq manager.example.com" \ + "id:100000001,phase:1,pass,nolog,t:none,t:lowercase,chain" + SecRule REQUEST_METHOD "@streq POST" \ + "t:none,chain" SecRule REQUEST_FILENAME "@streq /tigera-kibana/internal/bsearch" \ - "t:none,t:urlDecodeUni,t:normalizePath" + "t:none,t:urlDecodeUni,t:normalizePath,ctl:requestBodyAccess=Off" # The url argument reads as server-side request forgery. On the Manager host # only, remove that one argument from the two forgery rules. - SecRule REQUEST_HEADERS:Host "@streq manager.example.com" \ - "id:100000002,phase:1,pass,nolog,t:none,t:lowercase,\ - ctl:ruleRemoveTargetById=931100;ARGS:url,\ - ctl:ruleRemoveTargetById=934110;ARGS:url,chain" + SecRule SERVER_NAME "@streq manager.example.com" \ + "id:100000002,phase:1,pass,nolog,t:none,t:lowercase,chain" SecRule REQUEST_FILENAME "@beginsWith /tigera-kibana/" \ - "t:none,t:urlDecodeUni,t:normalizePath" + "t:none,t:urlDecodeUni,t:normalizePath,\ + ctl:ruleRemoveTargetById=931100;ARGS:url,\ + ctl:ruleRemoveTargetById=934110;ARGS:url" --- apiVersion: applicationlayer.projectcalico.org/v3 kind: GlobalWAFPolicy @@ -189,7 +191,14 @@ spec: name: kibana-exceptions ``` -If the cluster already has a `default` `GlobalWAFPolicy`, add the plugin to its existing `plugins` list rather than replacing the object. +If the cluster already has a `default` `GlobalWAFPolicy`, do **not** re-apply the block above — a second `kubectl apply` drops any field the file omits, so a `defaultAction: Block` would fall back to `Detect` for every policy that inherits it. Append the plugin with a patch instead: + +```bash +kubectl patch globalwafpolicy default --type=json \ + -p='[{"op":"add","path":"/spec/plugins/-","value":{"kind":"GlobalWAFPlugin","name":"kibana-exceptions"}}]' +``` + +Apply the full `GlobalWAFPolicy` block above only on a cluster that has no `default` yet. :::caution @@ -199,13 +208,19 @@ Trade-off: on the Manager gateway host, the WAF does not inspect the **body** of :::note -The plugin is cluster-scoped — through the `default` `GlobalWAFPolicy` it reaches every `WAFPolicy` — but each rule is pinned to the Manager gateway host with `REQUEST_HEADERS:Host`, so a second gateway serving the same paths is unaffected. Set the host in both rules to your Manager gateway hostname. +The plugin **loads** on every WAF-covered gateway and route: the controller merges `GlobalWAFPolicy/default` into every `WAFPolicy` in the cluster. The `SERVER_NAME` host match and the path match keep it **firing** only for Kibana on the Manager gateway, so a second gateway serving the same paths is unaffected. Set the host in both rules to your Manager gateway hostname. + +::: + +:::note + +There is no namespaced way to drop a single Core Rule Set rule — a `WAFPlugin` cannot remove or retarget a baseline rule (IDs `900000`–`999999` or the Tigera-managed range). The nearest namespaced alternative is a `WAFPolicy` with `action: Detect`, in the Manager gateway's namespace (`calico-system`, unless `Manager.spec.ingressGateway.gatewayNamespace` says otherwise), targeting the `calico-manager-route` `HTTPRoute`. The cost is that it turns the **whole** Manager gateway to Detect — it stops blocking for the entire Manager UI, not just Kibana — because the operator renders that route as one rule with no path match and reverts hand edits. Use the `GlobalWAFPlugin` above if you need to keep blocking everywhere else. ::: :::note -The rule IDs `100000001` and `100000002` sit above every reserved range. The search endpoint can differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm the search path, and add a rule per search endpoint rather than loosening the exact-path match. Because the body exception keys on the request (`POST` + host + path) and not on named rules, it keeps working as the Core Rule Set changes which rules match. +The rule IDs `100000001` and `100000002` are safe to reuse and to copy for other apps: namespace ID windows stop at `99,999,999` and the protected ranges (Core Rule Set and Tigera baseline) stop at `1,099,999`, so IDs from `100,000,000` up can't clash with a tenant's rules or a baseline rule. The search endpoint can differ between Kibana versions — read the `tigera_secure_ee_waf` log index for your cluster to confirm the search path, and add a rule per search endpoint rather than loosening the exact-path match. Because the body exception keys on the request (`SERVER_NAME` + `POST` + path) and not on named rules, it keeps working as the Core Rule Set changes which rules match. :::