From 90361775b95f8537726f0c5ab915417d22c5c41d Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 11:53:42 +0100 Subject: [PATCH 01/16] docs: add Gateway WAF (per-route & namespaced) section Add a Calico Enterprise docs section for the Gateway-API WAF feature (OWASP CRS via a Coraza WASM filter on Envoy Gateway): overview, concepts (families, global vs namespaced, merge order + enforcement), get-started (operator-native enable + quickstart), persona-oriented how-tos (CO baseline, AO attach/rules/validate, multi-tenant), and a CRD / Rego / SecRule-ID-range / status-condition reference, plus troubleshooting. - New section under threat/gateway-waf/ (16 pages) + sidebar category. - Deprecation note on deploying-waf-ingress-gateway (superseded by the new per-route/namespaced model). Refs: PMREQ-384, designs#25, cp#12137 (Rego engine, EV-6656). Docs: EV-6466. --- .../threat/deploying-waf-ingress-gateway.mdx | 6 + .../concepts/global-vs-namespaced.mdx | 61 ++++ .../concepts/merge-and-enforcement.mdx | 75 +++++ .../concepts/policies-plugins-validation.mdx | 53 ++++ .../threat/gateway-waf/get-started/enable.mdx | 99 ++++++ .../gateway-waf/get-started/quickstart.mdx | 265 ++++++++++++++++ .../how-to/attach-waf-to-a-route.mdx | 227 ++++++++++++++ .../gateway-waf/how-to/multi-tenant-setup.mdx | 284 ++++++++++++++++++ .../gateway-waf/how-to/set-a-baseline.mdx | 174 +++++++++++ .../gateway-waf/how-to/validate-config.mdx | 257 ++++++++++++++++ .../gateway-waf/how-to/write-custom-rules.mdx | 195 ++++++++++++ .../threat/gateway-waf/index.mdx | 70 +++++ .../threat/gateway-waf/reference/crds.mdx | 210 +++++++++++++ .../gateway-waf/reference/rego-validation.mdx | 81 +++++ .../reference/secrule-id-ranges.mdx | 70 +++++ .../reference/status-conditions.mdx | 82 +++++ .../threat/gateway-waf/troubleshooting.mdx | 201 +++++++++++++ sidebars-calico-enterprise.js | 46 +++ 18 files changed, 2456 insertions(+) create mode 100644 calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx create mode 100644 calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx create mode 100644 calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx create mode 100644 calico-enterprise/threat/gateway-waf/get-started/enable.mdx create mode 100644 calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx create mode 100644 calico-enterprise/threat/gateway-waf/index.mdx create mode 100644 calico-enterprise/threat/gateway-waf/reference/crds.mdx create mode 100644 calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx create mode 100644 calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx create mode 100644 calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx create mode 100644 calico-enterprise/threat/gateway-waf/troubleshooting.mdx diff --git a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx index 03aaf89acd..7289502342 100644 --- a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx +++ b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx @@ -13,6 +13,12 @@ This feature is tech preview. Tech preview features may be subject to significan ::: +:::caution + +This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/how-to/attach-waf-to-a-route.mdx). This page will be removed in a future release. + +::: + ## Prerequisites * [Calico Ingress Gateway](../networking/ingress-gateway/about-calico-ingress-gateway.mdx) is set up for your cluster. diff --git a/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx b/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx new file mode 100644 index 0000000000..976a5e6cda --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx @@ -0,0 +1,61 @@ +--- +description: How cluster-scoped and namespace-scoped Gateway WAF resources relate — ownership, the global singleton, and field inheritance. +--- + +# Global vs. namespaced + +Every Gateway WAF resource family has a cluster-scoped member and a namespace-scoped member. The split is deliberate: it maps the WAF configuration onto the two people who operate the cluster. + +| | Cluster-scoped | Namespace-scoped | +| :--- | :--- | :--- | +| Owner | Cluster operator (CO) | Application operator (AO) | +| Resources | `GlobalWAFPolicy`, `GlobalWAFPlugin`, `GlobalWAFValidationPolicy` | `WAFPolicy`, `WAFPlugin`, `WAFValidationPolicy` | +| Role | Set the baseline and the guardrails | Attach WAF to traffic and tune within the guardrails | + +The cluster operator *sets* the security posture; the application operator *tunes* it for their workloads. An AO can make their namespace stricter, and can be prevented from making it weaker through [validation policies](merge-and-enforcement.mdx#validation-and-enforcement). + +## The global policy is a singleton + +`GlobalWAFPolicy` is a cluster-wide singleton: it must be named `default`. It establishes the cluster baseline — default action, Core Rule Set state, and paranoia level — that every namespace inherits unless it overrides. + +`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons; you can create as many as you need. + +## Inheritance: namespaces override, they don't restart + +A `WAFPolicy` does not have to restate the whole baseline. Its `action` and `coreRuleSet` fields are optional — when unset, the namespace inherits the value from `GlobalWAFPolicy`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override the global default for this namespace + coreRuleSet: + paranoiaLevel: 2 # stricter than the baseline + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +| `WAFPolicy` field | When set | When unset | +| :--- | :--- | :--- | +| `action` | overrides the global `defaultAction` for this namespace | inherits `GlobalWAFPolicy.spec.defaultAction` | +| `coreRuleSet.state` | overrides the global state | inherits the global state | +| `coreRuleSet.paranoiaLevel` | overrides the global level | inherits the global level | + +## Plugins are additive, not overriding + +Plugins don't override each other — they stack. When a target is configured, the merger applies **all** referenced global plugins first (in `GlobalWAFPolicy.spec.plugins[]` order), then all referenced namespace plugins (in `WAFPolicy.spec.plugins[]` order). Order is array position; there is no priority field. + +This is what lets a cluster operator's baseline rules and an application operator's exceptions coexist in a single ruleset. The exact interleaving — and why the global `after` slot runs last — is covered in [Merge order and enforcement](merge-and-enforcement.mdx). + +## Validation differs by scope + +Validation is the one place where the two scopes behave differently rather than symmetrically: + +- `GlobalWAFValidationPolicy` can **enforce** (block a non-compliant configuration) or **audit** (warn only), and uses a `namespaceSelector` to choose which namespaces it governs. +- `WAFValidationPolicy` is **advisory only**. It reports violations on the policy status so the AO can self-correct, but it never blocks. Enforcement is a cluster-operator responsibility. diff --git a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx new file mode 100644 index 0000000000..00b9d156b7 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx @@ -0,0 +1,75 @@ +--- +description: How Gateway WAF assembles the merged ruleset, the order rules load in, and the two layers that keep configurations safe — the admission webhook and validation policies. +--- + +# Merge order and enforcement + +When a `WAFPolicy` attaches to a target, the controller gathers every policy and plugin that applies, merges them into a single ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each entry in `targetRefs[]` produces its own `EnvoyExtensionPolicy`, so a policy attached to three routes generates three filters. + +Three things govern the outcome: the **merge order** (how the ruleset is assembled), the **admission webhook** (what raw SecLang you're allowed to submit), and **validation policies** (whether the merged result is compliant). + +## The merged ruleset load order + +Directive order is not cosmetic — the Core Rule Set requires configuration variables to be set before the ruleset loads, and post-processing to run after. The merger assembles directives in this fixed order: + +**Engine setup** +1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. +2. Engine settings — audit logging and request-body access. + +**Plugin `config` (variable setup)** +3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. +4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. + +**CRS setup** +5. Core Rule Set setup — reads the `tx.*` variables set above. +6. Blocking paranoia level, from the merged paranoia level. +7. Detection paranoia level (always ≥ blocking level). + +**Pre-CRS rules (`before`)** +8. Global plugin `before` rules. +9. Namespace plugin `before` rules. + +**Core Rule Set** +10. The full OWASP Core Rule Set. + +**Custom rules (`rules`)** +11. Global plugin `rules`. +12. Namespace plugin `rules`. + +**Post-processing (`after`)** +13. Namespace plugin `after` rules. +14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. + +The pattern is global-before-namespace through the early slots, and namespace-before-global in the `after` slot. Within each plugin, its own slots always run in `config` → `before` → `rules` → `after` order. + +:::note + +When two `WAFPolicy` resources target the same route, $[prodname] follows the Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) default: the policy with the oldest `creationTimestamp` wins (ties broken by `namespace/name`). The loser is marked `Accepted=False` with reason `Conflicted`, and its directives are not merged. + +::: + +## Structural safety: the admission webhook + +Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at the moment you apply it. It parses each directive and rejects the resource if any directive fails one of three checks: + +| Check | Passes when | Rejected with | +| :--- | :--- | :--- | +| Directive is allowed | the directive type is on the allowlist (`SecRule`, `SecAction`, `SecRuleRemoveById`, and other safe directives) | `UnsafeDirective` | +| Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | +| Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | + +This is what stops one namespace from colliding with another's rule IDs, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived and which rule IDs are protected. + +## Validation and enforcement + +The admission webhook checks *raw SecLang*. Validation policies check the *merged result* — and they run after the merge precisely because some problems are only visible there. Two innocent plugins can combine to disable all SQL-injection rules; no admission-time check can see that, but a Rego rule evaluated against the merged directive list can. + +Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with a severity of `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: + +| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | +| :--- | :--- | :--- | :--- | +| `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | +| `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | +| `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | + +To write the rules themselves, see the [Rego validation reference](../reference/rego-validation.mdx). To read the outcome, see [Status conditions](../reference/status-conditions.mdx). diff --git a/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx b/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx new file mode 100644 index 0000000000..75df354b6b --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx @@ -0,0 +1,53 @@ +--- +description: Understand the three Gateway WAF resource families — policies, plugins, and validation policies — and when to use each. +--- + +# Policies, plugins, and validation + +Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group. They come as three families, and each family has a cluster-scoped resource (owned by the cluster operator) and a namespace-scoped resource (owned by the application operator). + +| Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | +| :--- | :--- | :--- | :--- | +| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, CRS state, paranoia, which plugins | +| **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | +| **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | + +## Policies — what to enforce + +A **policy** declares the high-level WAF configuration: the action mode (`Detect` or `Block`), whether the OWASP Core Rule Set is enabled, the paranoia level, and which plugins to load. + +- `GlobalWAFPolicy` is a cluster-wide singleton (`name: default`) that sets the baseline for every namespace. +- `WAFPolicy` is namespace-scoped. It attaches WAF to a Gateway or `HTTPRoute` through `targetRefs`, and can override the baseline action and paranoia for that namespace. + +Reach for a policy when you want to **turn WAF on for some traffic** and set its overall posture. See [Attach WAF to a route](../how-to/attach-waf-to-a-route.mdx). + +## Plugins — custom rules + +A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [CRS 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. + +- `GlobalWAFPlugin` holds organization-wide rules — security baselines, compliance rules. +- `WAFPlugin` holds application-specific rules and false-positive exceptions. + +Reach for a plugin when the Core Rule Set isn't enough — you need a **custom detection rule, a pre-processing step, or an exception** for a known false positive. See [Write custom rules](../how-to/write-custom-rules.mdx). + +## Validation policies — guardrails + +A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules that are evaluated against the *merged, effective* WAF configuration. They answer "is this configuration acceptable?" — for example, "the Core Rule Set must stay enabled" or "production namespaces must run in Block mode." + +- `GlobalWAFValidationPolicy` is enforced by the cluster operator. In `Enforce` mode a `Critical` violation blocks the configuration from being programmed; in `Audit` mode it only warns. A `namespaceSelector` scopes which namespaces it applies to. +- `WAFValidationPolicy` is **advisory only** — it surfaces violations on the policy status for the application operator to act on, but never blocks. There is no enforce mode at namespace scope. + +Reach for a validation policy when you want to **codify a requirement** rather than a rule. See [Validate configuration](../how-to/validate-config.mdx) and the [Rego validation reference](../reference/rego-validation.mdx). + +:::note + +Validation policies are not the same as the admission webhook. The webhook checks the *raw SecLang* you submit (safe directives, allowed rule IDs) at write time; validation policies check the *merged result* for compliance. See [Merge order and enforcement](merge-and-enforcement.mdx). + +::: + +## How they fit together + +These resources combine before they reach the data plane: all applicable policies and plugins are merged into one ordered ruleset, validated, and programmed into Envoy Gateway. To understand precedence and inheritance, read: + +- [Global vs. namespaced](global-vs-namespaced.mdx) — scope, ownership, and inheritance. +- [Merge order and enforcement](merge-and-enforcement.mdx) — how the merged ruleset is assembled and checked. diff --git a/calico-enterprise/threat/gateway-waf/get-started/enable.mdx b/calico-enterprise/threat/gateway-waf/get-started/enable.mdx new file mode 100644 index 0000000000..2d6661e4c7 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/get-started/enable.mdx @@ -0,0 +1,99 @@ +--- +description: Enable Gateway WAF by setting a single field on the GatewayAPI operator CR, which installs the six WAF CRDs and the admission webhook. +--- + +# Enable Gateway WAF + +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + +## Before you begin + +**Prerequisites:** + +- $[prodname] is installed with a valid license that includes the Gateway WAF feature. +- $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). + +:::note + +Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires the Ingress Gateway that $[prodname] manages. + +::: + +## Enable the feature + +Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. + +```yaml +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled # default is Disabled +``` + +Apply it: + +```bash +kubectl apply -f - <<'EOF' +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled +EOF +``` + +The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. + +## Verify + +Confirm the six CRDs are installed: + +```bash +kubectl get crd | grep applicationlayer.projectcalico.org +``` + +```text +globalwafplugins.applicationlayer.projectcalico.org +globalwafpolicies.applicationlayer.projectcalico.org +globalwafvalidationpolicies.applicationlayer.projectcalico.org +wafplugins.applicationlayer.projectcalico.org +wafpolicies.applicationlayer.projectcalico.org +wafvalidationpolicies.applicationlayer.projectcalico.org +``` + +All six CRDs must be present before you create any WAF resources. + +Next, create a baseline `GlobalWAFPolicy` and confirm $[prodname] accepts it. See [Set a baseline](../how-to/set-a-baseline.mdx). + +:::tip + +To confirm the admission webhook is active, run: + +```bash +kubectl get validatingwebhookconfigurations | grep waf +``` + +You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. + +::: + +## Troubleshoot + +If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](../troubleshooting.mdx) for diagnosis steps and the pull-secret configuration. + +## Next steps + +- [Quickstart](./quickstart.mdx) — block an attack in under five minutes. +- [Set a baseline](../how-to/set-a-baseline.mdx) — create the cluster-wide `GlobalWAFPolicy`. +- [Policies, plugins, and validation](../concepts/policies-plugins-validation.mdx) — understand the six resource types. diff --git a/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx b/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx new file mode 100644 index 0000000000..6d7dcf94da --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx @@ -0,0 +1,265 @@ +--- +description: Deploy a test app, attach Gateway WAF, and confirm it blocks a SQL injection and XSS attack in about five minutes. +--- + +# Quickstart + +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + +This guide walks you from a running Ingress Gateway to a blocked attack in about five minutes. + +## Before you begin + +**Prerequisites:** + +- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- You have a namespace where you can deploy a test application. + +:::tip + +This quickstart uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the tradeoffs. + +::: + +## Step 1: Deploy a test application + +Create an `httpbin` namespace with a deployment and service to act as the backend. + +```bash +kubectl apply -f - <<'EOF' +apiVersion: v1 +kind: Namespace +metadata: + name: httpbin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: httpbin + namespace: httpbin +spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + template: + metadata: + labels: + app: httpbin + spec: + containers: + - name: httpbin + image: kong/httpbin:0.1.0 + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: httpbin + namespace: httpbin +spec: + selector: + app: httpbin + ports: + - protocol: TCP + port: 80 + targetPort: 80 +EOF +``` + +Wait for the pod to be ready: + +```bash +kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s +``` + +## Step 2: Create a Gateway and HTTPRoute + +```bash +kubectl apply -f - <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: httpbin-gateway + namespace: httpbin +spec: + gatewayClassName: tigera-gateway-class + listeners: + - name: http + protocol: HTTP + port: 9080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin-route + namespace: httpbin +spec: + parentRefs: + - name: httpbin-gateway + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: httpbin + port: 80 +EOF +``` + +:::note + +Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod. + +::: + +Wait for the Gateway to be accepted: + +```bash +kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s +``` + +## Step 3: Confirm traffic flows + +Find the Envoy service that the Ingress Gateway created for this Gateway. Services are created in the `tigera-gateway` namespace. + +```bash +export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ + -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') +echo "Gateway service: $GATEWAY_SVC" +``` + +Send a test request from inside the cluster: + +```bash +kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get +``` + +```text +200 +``` + +Traffic is flowing through the Gateway with no WAF protection yet. + +## Step 4: Apply a GlobalWAFPolicy + +`GlobalWAFPolicy` sets the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode. + +```bash +kubectl apply -f - <<'EOF' +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +EOF +``` + +This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. + +## Step 5: Attach WAF to the Gateway + +Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway. + +```bash +kubectl apply -f - <<'EOF' +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: httpbin-waf + namespace: httpbin +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: httpbin-gateway +EOF +``` + +The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. + +Check that the policy was accepted and programmed: + +```bash +kubectl get wafpolicy httpbin-waf -n httpbin \ + -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' +``` + +```text +Licensed=True (LicenseValid) +Accepted=True (Accepted) +Validated=Unknown (NotEvaluated) +Programmed=True (Programmed) +Ready=True (Ready) +``` + +All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. + +If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](../troubleshooting.mdx). + +Allow a few seconds for Envoy to load the WASM filter before running the tests below. + +## Step 6: Test WAF blocking + +**Normal request — expect 200:** + +```bash +kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get +``` + +```text +200 +``` + +**SQL injection probe — expect 403:** + +```bash +kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" +``` + +```text +403 +``` + +**XSS probe — expect 403:** + +```bash +kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" +``` + +```text +403 +``` + +The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. CRS rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. + +## Clean up + +```bash +kubectl delete namespace httpbin +kubectl delete globalwafpolicies default +``` + +## Next steps + +- [Write custom rules](../how-to/write-custom-rules.mdx) — add application-specific rules with `WAFPlugin`. +- [Validate configuration](../how-to/validate-config.mdx) — codify security requirements with Rego validation policies. +- [Multi-tenant setup](../how-to/multi-tenant-setup.mdx) — cluster operator and application operator workflow with per-namespace governance. diff --git a/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx b/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx new file mode 100644 index 0000000000..8f22342767 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx @@ -0,0 +1,227 @@ +--- +description: Create a WAFPolicy with targetRefs to attach WAF inspection to a Gateway or HTTPRoute in your namespace. +--- + +# Attach WAF to a Gateway or route + +:::note[Application operator task] + +This page is for application operators who own namespace-scoped WAF resources and decide which of their Gateways and routes receive WAF inspection. + +::: + +WAF inspection does not start automatically when the cluster operator enables the feature. You activate it by creating a `WAFPolicy` in your namespace that points to one or more Gateway API targets. The policy inherits the cluster baseline and lets you layer namespace-specific overrides on top. + +## Prerequisites + +- The cluster operator has created a `GlobalWAFPolicy`. See [Set a cluster-wide baseline](./set-a-baseline.mdx). +- You have write access to `WAFPolicy` resources in your namespace. +- A `Gateway` or `HTTPRoute` in your namespace is already receiving traffic. See [About Calico Ingress Gateway](../../../networking/ingress-gateway/about-calico-ingress-gateway.mdx) if you have not set one up. + +## Understand the two attachment scopes + +`WAFPolicy.spec.targetRefs[]` accepts two kinds of targets. + +| `kind` in `targetRefs` | WAF covers | Use when | +| :--- | :--- | :--- | +| `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | +| `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | + +Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` on Envoy Gateway. A policy with two entries generates two filters — one per target. + +:::note + +`sectionName` (targeting a specific listener on a Gateway) is reserved for a future release. Omit it. + +::: + +## Step 1 — Create a WAFPolicy + +### Option A — Gateway-wide WAF + +This example attaches WAF to every route on the `team-gateway` Gateway. Because `action` and `coreRuleSet` are unset, both fields inherit the cluster baseline. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: team-gateway-waf + namespace: team-a +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: team-gateway +``` + +### Option B — Per-route WAF + +This example attaches WAF to a single `HTTPRoute` and overrides the action and paranoia level for this namespace. Unset fields (here, `coreRuleSet.state`) still inherit from the cluster baseline. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-api-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override: Block even if global baseline is Detect + coreRuleSet: + paranoiaLevel: 2 # stricter than the cluster default + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +### Combining both scopes in one policy + +You can list both a Gateway and specific HTTPRoutes in a single `WAFPolicy`: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: mixed-targets + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: payments-gateway + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: internal-api + action: Block +``` + +Up to 16 entries are allowed in `targetRefs`. + +Apply the policy: + +```bash +kubectl apply -f wafpolicy.yaml +``` + +## Step 2 — Add a namespace plugin (optional) + +If you need rules beyond the cluster baseline — such as a custom detection rule or a false-positive exception — create a `WAFPlugin` and reference it from `WAFPolicy.spec.plugins[]`: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: payments-rules + namespace: payments +spec: + description: "Payments-specific WAF rules" + rules: | + SecRule REQUEST_HEADERS:Content-Type "!@rx ^application/json" \ + "id:5600001,phase:1,deny,status:415,msg:'Only JSON accepted on payments API'" +--- +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-api-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block + plugins: + - kind: WAFPlugin + name: payments-rules +``` + +Rule IDs must fall within the ID range allocated to your namespace. See [Write custom rules](./write-custom-rules.mdx) for how to find your range and how the four plugin slots work. + +## Inheritance and field overrides + +Fields you set in `WAFPolicy` override the corresponding field in `GlobalWAFPolicy`. Fields you leave unset are inherited. + +| `WAFPolicy` field | When set | When unset | +| :--- | :--- | :--- | +| `action` | overrides `GlobalWAFPolicy.spec.defaultAction` | inherits the global default | +| `coreRuleSet.state` | overrides the global CRS state | inherits the global CRS state | +| `coreRuleSet.paranoiaLevel` | overrides the global paranoia level | inherits the global paranoia level | + +For the complete picture, including how global and namespace plugins stack, see [Global vs. namespaced](../concepts/global-vs-namespaced.mdx). + +## Conflict resolution + +Only one `WAFPolicy` may be the active policy for any given target at a time. If two `WAFPolicy` resources in the same namespace both target the same Gateway or HTTPRoute, $[prodname] follows the [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) tie-breaking rule: + +- The policy with the **oldest `creationTimestamp`** wins. +- The newer policy is marked `Accepted=False` with reason `Conflicted`. +- Only the winning policy's directives are merged and programmed. + +To resolve a conflict, delete the newer policy or update its `targetRefs` to point at a different target. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the full conflict model. + +## Step 3 — Verify the policy + +Check that the policy was accepted and programmed: + +```bash +kubectl get wafpolicy payments-api-waf -n payments +``` + +``` +NAME ACCEPTED PROGRAMMED AGE +payments-api-waf True True 45s +``` + +For detailed conditions — including validation violations or conflict reasons — inspect the full status: + +```bash +kubectl get wafpolicy payments-api-waf -n payments -o yaml +``` + +The `.status.conditions` field reports `Accepted`, `Programmed`, and any validation findings. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. + +You can also confirm that Envoy Gateway received the filter: + +```bash +kubectl get envoyextensionpolicies -n payments +``` + +Each `targetRefs` entry produces one `EnvoyExtensionPolicy`. A policy with two entries will show two resources. + +## Step 4 — Test the WAF + +Send a request that includes a classic SQL injection pattern. In `Block` mode this should return HTTP 403: + +```bash +curl -si "https:///api?id=1+OR+1%3D1" +``` + +``` +HTTP/1.1 403 Forbidden +content-length: 11 +content-type: text/plain + +Access denied +``` + +:::tip + +If the Gateway is running in `Detect` mode (the cluster default until you upgrade to `Block`), the request will be allowed through but the WAF event will appear in the Envoy Gateway logs. Check with: + +```bash +kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "waf\|coraza\|modsec" +``` + +::: + +:::caution + +If you get `ACCEPTED=False` with reason `ValidationFailed`, a `GlobalWAFValidationPolicy` is blocking your configuration. The status message will identify which policy and rule triggered the violation. Contact your cluster operator or see [Validate configuration](./validate-config.mdx) for the self-check workflow. + +::: + +## Next steps + +- **Add app-specific rules or exceptions** — write custom ModSecurity rules or suppress false positives with a `WAFPlugin`. See [Write custom rules](./write-custom-rules.mdx). +- **Self-validate your config** — check compliance against the cluster operator's requirements before they enforce it. See [Validate configuration](./validate-config.mdx). diff --git a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx new file mode 100644 index 0000000000..da4f49260c --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx @@ -0,0 +1,284 @@ +--- +description: Walk through an end-to-end Gateway WAF multi-tenant setup where a cluster operator sets a cluster-wide baseline and two application teams tune WAF independently within enforced guardrails. +--- + +# Multi-tenant setup + +:::note[Scenario] + +This guide walks through a single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. You will see the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. + +Prerequisites: Gateway WAF is enabled and a valid license is in place. See [Enable Gateway WAF](../get-started/enable.mdx). + +::: + +--- + +## Part 1: Cluster operator + +All resources in this section are created by the **cluster operator** using cluster-scoped credentials. + +### Set the baseline policy + +`GlobalWAFPolicy` is a singleton named `default`. It defines the security floor every namespace inherits. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 1 + plugins: + - name: org-security-baseline +``` + +This establishes Block mode and OWASP CRS at paranoia level 1 for every namespace. A `WAFPolicy` can raise the paranoia level or override the action, but the validation policy below prevents it from lowering either. + +### Add an org-wide plugin + +`GlobalWAFPlugin` resources carry custom rules that apply everywhere. The plugin referenced above adds a request-tracing requirement: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: org-security-baseline +spec: + description: "Org-wide rules — requires a correlation ID on every request" + rules: | + SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ + "id:800010,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" + after: | + SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ + "id:800099,phase:5,pass,log,msg:'WAF blocked request'" +``` + +The `after` slot runs last in the merge order, giving the cluster operator final control over anomaly scoring and audit logging. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the full load sequence. + +### Enforce compliance guardrails + +`GlobalWAFValidationPolicy` uses Rego rules to evaluate every `WAFPolicy` after the merge. Setting `mode: Enforce` means a `Critical` violation blocks programming — no Coraza filter is loaded — until the namespace policy is corrected. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: production-requirements +spec: + mode: Enforce + namespaceSelector: + matchLabels: + env: production + rules: + - name: must-use-block-mode + severity: Critical + rego: | + package waf.validation + violation[msg] { + input.config.action != "Block" + msg := "WAF policy must use Block mode in production namespaces" + } + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf.validation + violation[msg] { + input.config.crsState != "Enabled" + msg := "OWASP CRS must be enabled in production namespaces" + } + - name: minimum-paranoia-level + severity: Warning + rego: | + package waf.validation + violation[msg] { + input.config.paranoiaLevel < 1 + msg := "Paranoia level 1 is the recommended minimum" + } +``` + +The `namespaceSelector` targets only namespaces labelled `env: production`. The two `Critical` rules are hard gates; a `Warning` appears in `status.validation.failures` but does not block programming. For Rego variable reference, see [Rego validation](../reference/rego-validation.mdx). + +### Give application teams read access to global resources + +Application operators need to read `Global*` resources to understand what baseline they are inheriting. Grant read access with a `ClusterRole` and a per-namespace `RoleBinding`: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: waf-global-reader +rules: + - apiGroups: ["applicationlayer.projectcalico.org"] + resources: + - globalwafpolicies + - globalwafplugins + - globalwafvalidationpolicies + verbs: ["get", "list", "watch"] +``` + +Bind it in each team's namespace: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: waf-global-reader + namespace: team-a # repeat for payments +subjects: + - kind: Group + name: team-a-operators + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: waf-global-reader + apiGroup: rbac.authorization.k8s.io +``` + +:::note + +This grants read on cluster-scoped resources via a namespace-scoped `RoleBinding`. Application operators can see the baseline and validation policy, but cannot create or edit `Global*` resources. + +::: + +--- + +## Part 2: `team-a` operator + +The `team-a` operator attaches WAF to their Gateway and inherits the baseline. Because `coreRuleSet.state` is unset, it inherits `Enabled` from `GlobalWAFPolicy`. The operator raises paranoia to 2 but stays within the enforced guardrails. + +Label the namespace so the validation policy applies: + +```bash +kubectl label namespace team-a env=production +``` + +Create the `WAFPolicy`: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: team-a-waf + namespace: team-a +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: team-a-gateway + action: Block + coreRuleSet: + paranoiaLevel: 2 + # state is unset — inherited from GlobalWAFPolicy (Enabled) +``` + +After reconciliation the policy should reach `Ready=True`. The merged ruleset includes: +1. The org-wide `X-Request-ID` requirement from `GlobalWAFPlugin/org-security-baseline` +2. The OWASP CRS at paranoia level 2 (overriding the global level 1) +3. The `after` audit rule from the global plugin + +Verify the conditions: + +```bash +kubectl describe wafpolicy team-a-waf -n team-a +``` + +See [Status conditions](../reference/status-conditions.mdx) for what each condition means. + +--- + +## Part 3: `payments` operator + +The `payments` team runs a stricter configuration. They raise paranoia to 3 and add a namespace plugin that blocks GraphQL introspection. + +Label the namespace: + +```bash +kubectl label namespace payments env=production +``` + +First, find the namespace's allocated SecRule ID range so custom rule IDs land inside it: + +```bash +kubectl get namespace payments \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +``` + +The controller stamps this annotation on first use. All rule IDs in the plugin below must fall within this range. See [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived. + +Create the namespace plugin: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: payments-app-rules + namespace: payments +spec: + description: "Payments-specific rules — block GraphQL introspection" + rules: | + SecRule REQUEST_BODY "@rx (?i)__schema|__type" \ + "id:420010,phase:2,deny,status:403,msg:'GraphQL introspection blocked'" +``` + +Replace `420010` with an ID from your namespace's actual allocated range. + +Create the `WAFPolicy` referencing the plugin: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: payments-gateway + action: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 3 + plugins: + - name: payments-app-rules +``` + +### What happens if the payments team tries to weaken the baseline + +If the `payments` operator applies a policy with `action: Detect`, the `production-requirements` `GlobalWAFValidationPolicy` (in `Enforce` mode) fires the `must-use-block-mode` Critical rule. The controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation in `status.validation.failures`: + +```bash +kubectl get wafpolicy payments-waf -n payments \ + -o jsonpath='{.status.validation.failures}' | jq +``` + +No `EnvoyExtensionPolicy` is generated until the policy is corrected. The `payments` operator must restore `action: Block` or request a policy exception from the cluster operator. See [Validate config](./validate-config.mdx) for how to read and act on validation failures. + +--- + +## Result: isolation in practice + +Each team's WAF configuration is fully isolated by namespace: + +- **`team-a`** runs CRS at paranoia 2 with its own custom rules, scoped to its ID range. +- **`payments`** runs CRS at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. +- Neither team can disable the org-wide `X-Request-ID` rule or lower below Block mode. +- The cluster operator sees all validation violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — a single place to audit which namespaces are out of compliance. + +For the full ID-range isolation mechanism, see [SecRule ID ranges](../reference/secrule-id-ranges.mdx). For the cluster operator's cluster-wide violation view, see [Status conditions](../reference/status-conditions.mdx). + +## Related pages + +- [Concepts: Global vs. namespaced](../concepts/global-vs-namespaced.mdx) +- [Concepts: Merge order and enforcement](../concepts/merge-and-enforcement.mdx) +- [Reference: SecRule ID ranges](../reference/secrule-id-ranges.mdx) +- [Reference: Status conditions](../reference/status-conditions.mdx) +- [How-to: Attach WAF to a route](./attach-waf-to-a-route.mdx) +- [How-to: Write custom rules](./write-custom-rules.mdx) +- [How-to: Set a baseline](./set-a-baseline.mdx) +- [How-to: Validate config](./validate-config.mdx) +- [Troubleshooting](../troubleshooting.mdx) diff --git a/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx b/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx new file mode 100644 index 0000000000..ee50514c5c --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx @@ -0,0 +1,174 @@ +--- +description: Create a GlobalWAFPolicy and GlobalWAFPlugin to establish a cluster-wide WAF security baseline that every namespace inherits. +--- + +# Set a cluster-wide baseline + +:::note[Cluster operator task] + +This page is for cluster operators who own the `GlobalWAFPolicy` singleton and set the security posture that all namespaces inherit. + +::: + +Before any application operator can attach WAF to a route, you need a cluster baseline. The baseline defines the default action, Core Rule Set state, paranoia level, and any organization-wide custom rules. Application operators inherit these settings; they can tighten them, but a [validation policy](./validate-config.mdx) can prevent them from weakening them. + +## Prerequisites + +- The Gateway WAF feature gate is enabled on the `ApplicationLayer` resource. See [Enable Gateway WAF](../get-started/enable.mdx). +- You have cluster-admin rights (the `GlobalWAFPolicy` API is cluster-scoped). + +## Step 1 — Create the GlobalWAFPolicy singleton + +`GlobalWAFPolicy` must be named `default`. Applying any other name is rejected by the admission webhook. + +Start in `Detect` mode. `Detect` logs matched requests but allows them through, giving you time to measure false positives before you switch to `Block`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Detect # start here; promote to Block after validating false-positive rate + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +``` + +Apply it: + +```bash +kubectl apply -f globalwafpolicy.yaml +``` + +### Paranoia levels + +The OWASP Core Rule Set defines four paranoia levels. Each higher level adds more detection rules and will catch more attacks — at the cost of more false positives. Choose based on your organization's risk tolerance and the sensitivity of the workloads you are protecting. + +| Level | Posture | Typical use | +| :--- | :--- | :--- | +| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | +| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | +| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | +| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | + +:::tip + +Start at paranoia level 1 cluster-wide. Let application operators raise the level for their own namespaces if their traffic demands it. You can enforce a minimum level later with a [validation policy](./validate-config.mdx). + +::: + +### When to switch to Block + +Once you have confirmed the false-positive rate is acceptable, change `defaultAction` to `Block`: + +```yaml +spec: + defaultAction: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +``` + +In `Block` mode, $[prodname] returns HTTP 403 for any request that matches a WAF rule. + +## Step 2 — Add organization-wide rules with a GlobalWAFPlugin + +A `GlobalWAFPlugin` holds custom ModSecurity rules that apply to every namespace through the global policy's `plugins[]` list. Create the plugin first, then reference it from `GlobalWAFPolicy`. + +Rules are organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — that map to distinct points in the load order. See [Write custom rules](./write-custom-rules.mdx) for slot semantics and ID-range requirements. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: org-baseline +spec: + description: "Organization-wide security baseline rules" + config: | + # Slot 1 — set tx.* variables before CRS loads + SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | + # Slot 2 — pre-CRS rules; run before the Core Rule Set + SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ + "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" + SecRule TX:ENFORCE_HTTPS "@eq 1" "" + rules: | + # Slot 3 — custom rules that run alongside CRS + SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ + "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" + after: | + # Slot 4 — post-CRS processing; cluster operator's after slot runs last + SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ + "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" +``` + +Apply the plugin: + +```bash +kubectl apply -f globalwafplugin.yaml +``` + +Reference it from `GlobalWAFPolicy.spec.plugins[]`: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Detect + coreRuleSet: + state: Enabled + paranoiaLevel: 1 + plugins: + - kind: GlobalWAFPlugin + name: org-baseline +``` + +`plugins[]` is an ordered list. The merger processes plugins in array position order. Add more `GlobalWAFPlugin` entries to the array as your organization grows — up to 64 total. + +:::note + +You can add `WAFPlugin` entries to `GlobalWAFPolicy.spec.plugins[]` as well. For the complete merge order — including when global `after` slots run — see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx). + +::: + +## Step 3 — Verify the baseline + +Check that the resource was accepted: + +```bash +kubectl get globalwafpolicy default +``` + +``` +NAME ACCEPTED AGE +default True 30s +``` + +For detailed status — including any validation violations — inspect the status conditions: + +```bash +kubectl get globalwafpolicy default -o yaml +``` + +Look at `.status.conditions`. An `Accepted=True` condition with reason `Reconciled` means the baseline is programmed. If a [validation policy](../reference/status-conditions.mdx) blocked the configuration, you will see `Accepted=False` with a `ValidationFailed` reason and a message describing the violation. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. + +## How namespace inheritance works + +Every namespace that has a `WAFPolicy` attached to a target inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy`. A `WAFPolicy` can override any of these fields for its namespace; unset fields fall back to the global value. + +``` +GlobalWAFPolicy (baseline) + └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 + └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy +``` + +For a full account of which fields inherit and which override, see [Global vs. namespaced](../concepts/global-vs-namespaced.mdx). + +## Next steps + +- **Enforce compliance** — codify your requirements (minimum paranoia level, Block-only production) with a `GlobalWAFValidationPolicy`. See [Validate configuration](./validate-config.mdx). +- **Let teams attach WAF** — share [Attach WAF to a Gateway or route](./attach-waf-to-a-route.mdx) with your application operators. +- **Multi-tenant setup** — if you manage many teams, see [Multi-tenant setup](./multi-tenant-setup.mdx) for namespace selector patterns and ID-range allocation. diff --git a/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx b/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx new file mode 100644 index 0000000000..c6d0f13e06 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx @@ -0,0 +1,257 @@ +--- +description: Write Rego validation rules to audit or enforce organizational WAF standards across namespaces, and read violation results from resource status. +--- + +# Validate configuration with Rego + +Gateway WAF has two guardrail layers. The admission webhook checks raw SecLang at apply time (structural safety). Validation policies check the **merged result** at reconcile time — after all policies and plugins have been combined — so they can catch problems that are only visible in the assembled directive list. + +Validation rules are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), evaluated against a structured input document that describes the effective configuration. This page shows you how to write and apply them. For the full input schema and the violations contract, see [Rego validation](../reference/rego-validation.mdx). + +## How enforcement works + +There are two validation resources, with different scope and enforcement power: + +| Resource | Scope | Can block? | Persona | +| :--- | :--- | :--- | :--- | +| `GlobalWAFValidationPolicy` | Cluster-wide (optional `namespaceSelector`) | Yes — in `Enforce` mode with `Critical` severity | Cluster operator | +| `WAFValidationPolicy` | Namespace | No — advisory only | Application operator | + +The enforcement matrix is in [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#validation-and-enforcement). + +--- + +## Cluster operator: GlobalWAFValidationPolicy + +:::note[Cluster operator] + +`GlobalWAFValidationPolicy` is a cluster-scoped resource. Only cluster operators can create or modify it. It applies to every namespace, or to a label-selected subset via `namespaceSelector`. + +::: + +A `GlobalWAFValidationPolicy` carries a list of Rego rules and an `enforcementMode`: + +- `Audit` (default) — violations are recorded on status, but the WAF filter is still programmed. +- `Enforce` — a `Critical` violation blocks the filter from being programmed. `Warning` and `Info` violations are recorded on status regardless of mode. + +### Example: organizational security requirements + +The following policy enforces three requirements for all namespaces labeled `env: production`, and audits a fourth: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: production-requirements +spec: + enforcementMode: Enforce + namespaceSelector: + matchLabels: + env: production + rules: + + # Critical: CRS must be enabled + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } + + # Critical: production namespaces must use Block mode + - name: block-mode-required + severity: Critical + rego: | + package waf + violations[{"msg": "WAF must be in Block mode for production"}] { + input.config.action != "Block" + } + + # Critical: minimum paranoia level + - name: min-paranoia-level + severity: Critical + rego: | + package waf + violations[{"msg": msg}] { + input.config.paranoiaLevel < 2 + msg := sprintf( + "paranoia level %d is below the production minimum of 2", + [input.config.paranoiaLevel] + ) + } + + # Warning: advise against disabling RCE rules (does not block) + - name: rce-rules-intact + severity: Warning + rego: | + package waf + violations[{"msg": msg}] { + d := input.directives[_] + startswith(d, "SecRuleRemoveById") + re_match(`93[0-9]{4}`, d) + msg := sprintf("RCE rule removal detected in merged config: %s", [d]) + } +``` + +:::tip + +Start every new `GlobalWAFValidationPolicy` in `Audit` mode. Review the violations that appear on existing `WAFPolicy` status before switching to `Enforce`. This avoids blocking teams whose configurations predate the new policy. + +::: + +### Scoping with namespaceSelector + +Leave `namespaceSelector` empty (or omit it) to apply the policy to every namespace. Use it to target a subset: + +```yaml +spec: + namespaceSelector: + matchLabels: + compliance: pci-dss # only namespaces with this label +``` + +Multiple `GlobalWAFValidationPolicy` objects can coexist. Every matching policy's rules are evaluated; a single `Critical` violation from any of them (when in `Enforce` mode) is enough to block programming. + +### PCI-DSS example + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: pci-dss-requirements +spec: + enforcementMode: Enforce + namespaceSelector: + matchLabels: + compliance: pci-dss + rules: + + - name: pci-crs-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "PCI-DSS requires OWASP CRS to be enabled"}] { + input.config.crsState != "Enabled" + } + + - name: pci-block-mode + severity: Critical + rego: | + package waf + violations[{"msg": "PCI-DSS requires WAF Block mode"}] { + input.config.action != "Block" + } + + - name: pci-no-rule-removal + severity: Critical + rego: | + package waf + violations[{"msg": msg}] { + d := input.directives[_] + startswith(d, "SecRuleRemoveById") + msg := sprintf("PCI-DSS forbids disabling any CRS rules: %s", [d]) + } +``` + +--- + +## Application operator: WAFValidationPolicy + +:::note[Application operator] + +`WAFValidationPolicy` is namespace-scoped. It is advisory only — violations appear on status but never block the WAF filter from being programmed. Use it as a self-check for your namespace's configuration. + +::: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFValidationPolicy +metadata: + name: team-self-check + namespace: platform +spec: + rules: + + - name: graphql-plugin-active + severity: Warning + rego: | + package waf + violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { + not "graphql-waf" in input.source.namespacePlugins + } +``` + +Because `WAFValidationPolicy` is advisory, it is safe to define your own standards here that are stricter than the cluster baseline — for example, requiring a minimum paranoia level higher than the global default. Violations surface on the `WAFPolicy` status and prompt the author to fix the config, but they do not interrupt traffic. + +--- + +## Reading violation results + +Violations from all matching validation policies appear in the status conditions of the affected `WAFPolicy`. Check them with: + +```bash +kubectl get wafpolicy -n -o yaml +``` + +A blocked policy looks like: + +```yaml +status: + conditions: + - type: Ready + status: "False" + reason: ValidationFailed + message: > + GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": + WAF must be in Block mode for production + - type: Accepted + status: "True" +``` + +An audit violation (filter still programmed) looks like: + +```yaml +status: + conditions: + - type: Ready + status: "True" + - type: ValidationWarning + status: "True" + reason: AuditViolation + message: > + GlobalWAFValidationPolicy/production-requirements rule "rce-rules-intact" [Warning]: + RCE rule removal detected in merged config: SecRuleRemoveById 932100 +``` + +See [Status conditions](../reference/status-conditions.mdx) for the complete condition type reference. + +--- + +## Authoring Rego rules + +Each rule in `spec.rules[].rego` must: + +1. Declare `package waf`. +2. Define a `violations` rule that produces a set of objects, each with a `msg` field. + +You can inspect the full directive list to catch combinations that individual fields won't reveal: + +```rego +package waf + +violations[{"msg": msg}] { + d := input.directives[_] + startswith(d, "SecRuleRemoveById") + re_match(`94[0-9]{4}`, d) # targets a SQL-injection CRS rule + msg := sprintf("SQLi rule removal in merged config: %s", [d]) +} +``` + +For the complete input document schema, the violations contract, and pass/fail worked examples, see [Rego validation](../reference/rego-validation.mdx). + +:::tip + +Write your Rego rules against the input schema in [Rego validation](../reference/rego-validation.mdx) and test them locally with `opa eval` before applying to the cluster. A syntax error in a rule causes the entire policy to be marked invalid; the status reports which rule failed to parse. + +::: diff --git a/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx new file mode 100644 index 0000000000..d0634ba61b --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx @@ -0,0 +1,195 @@ +--- +description: Write ModSecurity rules in a WAFPlugin to extend or tune the WAF for your application, and attach the plugin to a WAFPolicy. +--- + +# Write custom rules with plugins + +:::note[Application operator task] + +This page is for **application operators** who manage WAF configuration within a namespace. Cluster operators who want organization-wide rules should create a `GlobalWAFPlugin` and reference it from `GlobalWAFPolicy.spec.plugins[]` — the CRD fields are identical; only scope differs. + +::: + +A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots. The slots map to CRS 4's plugin file structure and determine when your directives run relative to the OWASP Core Rule Set. + +## The four slots + +The merger assembles every plugin's slots into a fixed load order. Understanding that order is the key to knowing which slot to use. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the complete sequence. + +### `config` — variable setup + +**Runs before CRS setup.** Use `config` to set `tx.*` transaction variables that the CRS reads during its own initialization. Examples: tuning anomaly score thresholds, enabling optional CRS features, or setting application-specific flags. + +```yaml +config: | + SecAction \ + "id:420001,phase:1,pass,nolog,\ + setvar:tx.paranoia_level=2,\ + setvar:tx.anomaly_score_threshold=5" +``` + +If you write a rule in `config` that fires on a request variable (such as a request header), place it in `before` instead — at `config` time the request has not been read yet. + +### `before` — pre-CRS rules + +**Runs after variable setup but before the CRS loads.** Use `before` for rules that should execute before CRS analysis: early-exit allowlisting for trusted paths, request normalization, or setting flags that later CRS rules or your own rules can read. + +```yaml +before: | + SecRule REQUEST_URI "@beginsWith /internal/health" \ + "id:420010,phase:1,pass,nolog,ctl:ruleEngine=Off" +``` + +:::caution + +`ctl:ruleEngine=Off` inside a chained rule or as a per-request action disables the engine for that transaction only. Disabling it globally via a top-level `SecRuleEngine Off` directive is not permitted and will be rejected by the admission webhook. + +::: + +### `rules` — custom rules alongside CRS + +**Runs after the full CRS has executed.** This is a Tigera extension to the CRS plugin model. Use `rules` for application-specific detections — rules that complement the CRS rather than pre-empting it. Because they run after the CRS, they can also read and act on anomaly scores. + +```yaml +rules: | + SecRule REQUEST_BODY "@rx __schema|__type" \ + "id:420020,phase:2,deny,status:403,\ + msg:'GraphQL introspection blocked',\ + logdata:'URI=%{REQUEST_URI}'" +``` + +### `after` — post-CRS processing + +**Runs last.** Use `after` for false-positive suppression, response inspection, or logging augmentation. Because `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, the cluster operator has the final say on any post-processing — see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#the-merged-ruleset-load-order). + +```yaml +after: | + SecRuleRemoveById 420020 +``` + +:::note + +`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected CRS baseline rules regardless of slot — the admission webhook will reject the attempt. + +::: + +## Rule IDs and your namespace's range + +Every `SecRule` and `SecAction` must have an ID. Gateway WAF assigns each namespace a contiguous block of 10,000 IDs and enforces it at admission time. Before writing any rule, find your range: + +```bash +kubectl get namespace \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +# example output: 420000-429999 +``` + +Pick all your rule IDs from inside this range. The annotation is populated the first time any plugin is referenced in your namespace. + +The admission webhook rejects resources with: + +- Rule IDs outside your allocated range (`IDOutsideRange`) +- `SecRuleRemoveById` targeting a protected CRS baseline rule (`ProtectedIDRemoval`) +- Directives not on the allowlist (`UnsafeDirective`) + +See [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for protected ranges, the derivation algorithm, and the full allowlist. + +## Worked example + +The following `WAFPlugin` uses all four slots to protect a GraphQL API: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: graphql-waf + namespace: platform +spec: + description: "GraphQL introspection guard and false-positive exceptions" + + config: | + # Flag the application type so later rules can branch on it. + SecAction \ + "id:420001,phase:1,pass,nolog,\ + setvar:tx.is_graphql=1" + + before: | + # Allowlist the schema endpoint used by the internal schema registry — + # skip WAF analysis entirely for that specific path. + SecRule REQUEST_URI "@streq /graphql/schema-registry" \ + "id:420010,phase:1,pass,nolog,\ + ctl:ruleEngine=Off,\ + tag:allowlist" + + rules: | + # Block introspection queries (production guard). + SecRule TX:IS_GRAPHQL "@eq 1" \ + "id:420020,phase:2,deny,status:403,\ + msg:'GraphQL introspection blocked',\ + logdata:'body_snippet=%{MATCHED_VAR}',\ + chain" + SecRule REQUEST_BODY "@rx __schema|__type" "" + + # Block suspicious user-agents known to scan GraphQL APIs. + SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ + "id:420021,phase:1,deny,status:403,\ + msg:'Known GraphQL scanner blocked',\ + logdata:'ua=%{MATCHED_VAR}'" + + after: | + # Remove a false positive: CRS rule 941130 fires on a legitimate + # Content-Type value used by our GraphQL client library. + SecRuleRemoveById 941130 +``` + +:::tip + +The `@pmf` operator loads a data file. Data files must be bundled into the Coraza WASM filter image. Ask your cluster operator whether a custom data file is available, or use `@pm` (phrase match inline) for a small list of strings instead. + +::: + +## Attaching the plugin to a WAFPolicy + +A plugin has no effect until a `WAFPolicy` references it. Add the plugin to `spec.plugins[]`; load order is array position. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: platform-api-waf + namespace: platform +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: graphql-api + action: Block + plugins: + - kind: WAFPlugin + name: graphql-waf # ← your plugin above +``` + +If your policy also references a `GlobalWAFPlugin`, the global plugin's slots load before the namespace plugin's slots at each position in the merge order. See [Attach WAF to a route](./attach-waf-to-a-route.mdx) for full `targetRefs` options. + +## CRS and SecLang references + +Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The CRS documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. + +:::note + +Not all SecLang directives are permitted. Directives that read from the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](../reference/secrule-id-ranges.mdx#allowed-directives) for the allowlist. + +::: + +## Verify with status conditions + +After applying, check that the plugin was accepted and that the generated `WAFPolicy` reconciled without errors: + +```bash +# Check the plugin was accepted +kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" + +# Check the WAFPolicy reconciled and the filter was programmed +kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" +``` + +A `Ready=True` condition on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed in Envoy Gateway. If the policy is blocked by a validation rule, the status explains which rule fired and why. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx new file mode 100644 index 0000000000..4e008f1261 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -0,0 +1,70 @@ +--- +description: Protect Calico Ingress Gateway traffic with a per-route and per-namespace web application firewall, built on the OWASP Core Rule Set and delivered as a Coraza WebAssembly filter on Envoy Gateway. +hide_table_of_contents: true +--- + +import { DocCardLink, DocCardLinkLayout } from '/src/___new___/components'; + +# Gateway WAF + +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + +## Big picture + +Protect the traffic entering your cluster through $[prodname] Ingress Gateway with a web application firewall (WAF) that you attach per Gateway or per route, and tune per namespace. + +## Value + +Edge WAFs traditionally apply one ruleset to everything behind the gateway. Gateway WAF takes a Kubernetes-native, multi-tenant approach: a cluster operator sets a security baseline once, and each application team layers its own rules and exceptions on top — scoped to their own Gateways and `HTTPRoute`s, within guardrails the cluster operator defines. + +Gateway WAF runs the industry-standard [OWASP ModSecurity Core Rule Set (CRS) v4](https://owasp.org/www-project-modsecurity-core-rule-set/) as a [Coraza](https://coraza.io/) WebAssembly filter inside Envoy Gateway — no sidecars, and no traffic ever leaves the proxy for inspection. + +## How it works + +Gateway WAF is configured entirely through Kubernetes resources. The controller merges every policy and plugin that applies to a target, generates an ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy` that loads the Coraza WASM filter. + +```text + write time reconcile time data plane + ┌────────────────────┐ ┌─────────────────────────────────┐ ┌──────────────────────┐ + │ admission webhook │ │ merge ─▶ ordered SecLang ─▶ │ pass │ EnvoyExtensionPolicy │ + │ (allowlist + per-ns │ ──apply─▶│ Rego validation │ ─────▶ │ (Coraza WASM filter) │ ─▶ request + │ SecRule ID range) │ ok │ │ │ on Envoy Gateway │ + └────────────────────┘ └─────────────────────────────────┘ └──────────────────────┘ + │ fail (Enforce + Critical) + ▼ + no filter programmed + (status reports the violation) + + inputs to merge: GlobalWAFPolicy + WAFPolicy · GlobalWAFPlugin + WAFPlugin + guardrails: admission webhook (structural) · validation policies (compliance, Rego) +``` + +## Who does what + +Gateway WAF splits cleanly across two personas. Cluster-scoped resources belong to the **cluster operator (CO)**; namespace-scoped resources belong to the **application operator (AO)**. + +| Task | Cluster operator (CO) | Application operator (AO) | +| :--- | :--- | :--- | +| Enable the feature (license + `GatewayAPI` gate) | ✓ | | +| Cluster-wide baseline — action, CRS, paranoia | `GlobalWAFPolicy` (singleton) | | +| Organization-wide custom rules | `GlobalWAFPlugin` | | +| Compliance guardrails (audit or enforce) | `GlobalWAFValidationPolicy` | | +| Attach WAF to a Gateway or `HTTPRoute` | | `WAFPolicy` (`targetRefs`) | +| Per-namespace overrides (action, paranoia) | | `WAFPolicy` | +| App-specific rules and false-positive exceptions | | `WAFPlugin` | +| Self-check namespace config (advisory) | | `WAFValidationPolicy` | + +Each resource family — Policy, Plugin, and Validation — comes as a cluster-scoped/namespace-scoped pair. See [Policies, plugins, and validation](concepts/policies-plugins-validation.mdx) for when to reach for each. + +## Next steps + + + + + + + diff --git a/calico-enterprise/threat/gateway-waf/reference/crds.mdx b/calico-enterprise/threat/gateway-waf/reference/crds.mdx new file mode 100644 index 0000000000..0784715c74 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/reference/crds.mdx @@ -0,0 +1,210 @@ +--- +description: Field reference for the six Gateway WAF custom resources, grouped by family with cluster-scoped and namespace-scoped examples. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# CRD reference + +Gateway WAF defines six custom resources in the `applicationlayer.projectcalico.org/v3` API group. Each family has a cluster-scoped resource and a namespace-scoped resource that share almost all of their fields. + +| Resource | Scope | Short name | Persona | Purpose | +| :--- | :--- | :--- | :--- | :--- | +| `GlobalWAFPolicy` | Cluster | `gwafp` | CO | Cluster-wide baseline (singleton, `name: default`) | +| `WAFPolicy` | Namespace | `wafp` | AO | Attach WAF to a target; per-namespace overrides | +| `GlobalWAFPlugin` | Cluster | `gwafplugin` | CO | Organization-wide custom rules | +| `WAFPlugin` | Namespace | `wafplugin` | AO | App-specific rules and exceptions | +| `GlobalWAFValidationPolicy` | Cluster | `gwafvp` | CO | Compliance guardrails (enforce or audit) | +| `WAFValidationPolicy` | Namespace | `wafvp` | AO | Advisory self-validation | + +## Policy family + +`GlobalWAFPolicy` sets the cluster baseline; `WAFPolicy` attaches WAF to a target and overrides the baseline for its namespace. + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default # singleton: name MUST be "default" +spec: + defaultAction: Block # Detect | Block + coreRuleSet: + state: Enabled # Enabled | Disabled + paranoiaLevel: 1 # 1 (balanced) to 4 (paranoid) + plugins: # order = array position + - kind: GlobalWAFPlugin + name: security-baseline +``` + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: # attach to a Gateway and/or HTTPRoute + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + - group: gateway.networking.k8s.io + kind: Gateway + name: team-gateway + action: Block # optional; nil = inherit global defaultAction + coreRuleSet: + paranoiaLevel: 2 # optional; nil = inherit + plugins: + - kind: WAFPlugin + name: api-exceptions +``` + + + + +
+Field reference — Policy family + +| Field | Type | Resource | Notes | +| :--- | :--- | :--- | :--- | +| `defaultAction` | `Detect` \| `Block` | GlobalWAFPolicy | Cluster default; defaults to `Detect` | +| `action` | `Detect` \| `Block` | WAFPolicy | Optional; **nil = inherit** the global default | +| `coreRuleSet.state` | `Enabled` \| `Disabled` | both | Defaults to `Enabled`; nil on WAFPolicy = inherit | +| `coreRuleSet.paranoiaLevel` | integer 1–4 | both | Defaults to `1`; nil on WAFPolicy = inherit | +| `plugins[]` | list of `{kind, name}` | both | `kind`: `WAFPlugin` \| `GlobalWAFPlugin`; order = array position; max 64 | +| `targetRefs[]` | Gateway API target refs | **WAFPolicy only** | `kind`: `Gateway` \| `HTTPRoute`; min 1, max 16. `sectionName` is reserved for a future release | + +
+ +## Plugin family + +`GlobalWAFPlugin` and `WAFPlugin` have identical specs. Each holds custom ModSecurity rules in four lifecycle slots. See [Write custom rules](../how-to/write-custom-rules.mdx) for slot semantics. + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: security-baseline +spec: + description: "Cluster security baseline" + config: | # 1. set tx.* variables + SecAction "id:1234001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | # 2. pre-CRS processing + SecRule REQUEST_URI "@beginsWith /webhooks/" \ + "id:1234010,phase:1,pass,nolog,ctl:ruleRemoveById=942200" + rules: | # 3. custom rules alongside CRS + SecRule REQUEST_HEADERS:User-Agent "@pmFromFile bad-bots.data" \ + "id:1234020,phase:1,deny,status:403,msg:'Blocked bot'" + after: | # 4. post-CRS processing + SecAction "id:1234030,phase:5,pass,nolog,log" +``` + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: api-exceptions + namespace: payments +spec: + description: "GraphQL introspection guard" + config: | + SecAction "id:5678001,phase:1,pass,nolog,setvar:tx.graphql_max_depth=10" + rules: | + SecRule REQUEST_BODY "@rx __schema" \ + "id:5678020,phase:2,deny,status:403,msg:'Introspection blocked'" +``` + + + + +
+Field reference — Plugin family + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `description` | string | Human-readable purpose; max 1024 chars | +| `config` | string (SecLang) | Slot 1 — `tx.*` variable setup, runs before CRS setup; max 65536 chars | +| `before` | string (SecLang) | Slot 2 — pre-CRS rules; max 65536 chars | +| `rules` | string (SecLang) | Slot 3 — custom rules alongside CRS (Tigera extension); max 65536 chars | +| `after` | string (SecLang) | Slot 4 — post-CRS rules; max 65536 chars | + +Rule IDs must fall in your namespace's allocated range — see [SecRule ID ranges](secrule-id-ranges.mdx). + +
+ +## Validation family + +`GlobalWAFValidationPolicy` can enforce; `WAFValidationPolicy` is advisory only. Both carry a list of Rego rules. See [Rego validation](rego-validation.mdx) to author the rules. + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: security-requirements +spec: + enforcementMode: Enforce # Enforce | Audit (default Audit) + namespaceSelector: # optional; empty = all namespaces + matchLabels: + env: production + rules: + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } +``` + + + + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFValidationPolicy +metadata: + name: team-self-check + namespace: payments +spec: + # no enforcementMode and no namespaceSelector — advisory only + rules: + - name: paranoia-minimum + severity: Warning + rego: | + package waf + violations[{"msg": sprintf("paranoia %d below 2", [input.config.paranoiaLevel])}] { + input.config.paranoiaLevel < 2 + } +``` + + + + +
+Field reference — Validation family + +| Field | Type | Resource | Notes | +| :--- | :--- | :--- | :--- | +| `enforcementMode` | `Audit` \| `Enforce` | **GlobalWAFValidationPolicy only** | Defaults to `Audit`. `Enforce` blocks on a `Critical` violation | +| `namespaceSelector` | label selector | **GlobalWAFValidationPolicy only** | Empty = all namespaces | +| `rules[]` | list of `ValidationRule` | both | At least one required | +| `rules[].name` | string | both | 1–253 chars | +| `rules[].rego` | string (Rego module) | both | Must declare `package waf` and a `violations` rule; max 16384 chars | +| `rules[].severity` | `Info` \| `Warning` \| `Critical` | both | Defaults to `Warning` | + +
diff --git a/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx b/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx new file mode 100644 index 0000000000..46971c78b1 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx @@ -0,0 +1,81 @@ +--- +description: Author Rego validation rules for Gateway WAF — the input document schema, the violations contract, and worked pass/fail examples. +--- + +# Rego validation + +A validation policy carries one or more rules written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), the Open Policy Agent language. Each rule is evaluated **after** the WAF configuration is merged, against the effective result — so a rule can reason about the final action, the final paranoia level, and the complete list of merged directives, regardless of which policy or plugin contributed them. + +This page is the authoring contract. To attach rules to a cluster or namespace, see [Validate configuration](../how-to/validate-config.mdx); for how a violation affects reconciliation, see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#validation-and-enforcement). + +## The input document + +Every rule is evaluated against the same `input` document, which describes the merged configuration and its provenance: + +| Path | Type | Meaning | +| :--- | :--- | :--- | +| `input.config.action` | string | Effective action after merge — `Detect` or `Block` | +| `input.config.crsState` | string | Core Rule Set state — `Enabled` or `Disabled` | +| `input.config.paranoiaLevel` | integer | Effective paranoia level, 1–4 | +| `input.directives` | array of string | The complete, ordered list of merged ModSecurity directives | +| `input.source.globalPolicy` | string | Name of the global policy applied (always `default`) | +| `input.source.namespacePolicy` | string | Name of the `WAFPolicy`, if any | +| `input.source.globalPlugins` | array of string | Global plugin names, in load order | +| `input.source.namespacePlugins` | array of string | Namespace plugin names, in load order | + +## The violations contract + +A rule's `rego` field must: + +1. Declare `package waf`. +2. Define a `violations` rule that produces a set of objects, each with at least a `msg` field. + +A namespace with **no** violations is compliant. Any object added to `violations` is a finding; its `msg` is surfaced on the policy status, and any extra fields you include are passed through alongside it. + +```rego +package waf # required — the engine evaluates this package + +# Each block that adds to `violations` is one check. +violations[{"msg": msg}] { # a violation is an object with a msg field + input.config.crsState != "Enabled" # the condition that is NOT allowed + msg := "OWASP CRS must be enabled" +} + +violations[{"msg": msg}] { + input.config.paranoiaLevel < 2 # body lines are ANDed; all must hold + msg := sprintf("paranoia level %d is below the minimum of 2", [input.config.paranoiaLevel]) +} +``` + +You can also inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule: + +```rego +package waf + +violations[{"msg": msg}] { + d := input.directives[_] # iterate the merged directives + startswith(d, "SecRuleRemoveById") + re_match(`94[0-9]{4}`, d) # a CRS SQL-injection rule (942xxx) + msg := sprintf("removes a baseline SQLi rule: %s", [d]) +} +``` + +## Worked examples + +Given the rules above, here is how different merged configurations evaluate: + +| Effective configuration | `violations` | +| :--- | :--- | +| `crsState: Enabled`, `paranoiaLevel: 2` | empty — compliant | +| `crsState: Disabled` | `"OWASP CRS must be enabled"` | +| `paranoiaLevel: 1` | `"paranoia level 1 is below the minimum of 2"` | +| a plugin with `SecRuleRemoveById 942100` | `"removes a baseline SQLi rule: ..."` | + +## Severity + +Each rule has a `severity` of `Info`, `Warning`, or `Critical` (default `Warning`). Severity decides the consequence of a violation, not whether it fires: + +- In a `GlobalWAFValidationPolicy` set to `Enforce`, a `Critical` violation blocks the configuration from being programmed. +- In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. + +See the [enforcement table](../concepts/merge-and-enforcement.mdx#validation-and-enforcement) for the full matrix. diff --git a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx new file mode 100644 index 0000000000..914e3d1827 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx @@ -0,0 +1,70 @@ +--- +description: How Gateway WAF assigns each namespace a SecRule ID range, which rule IDs are protected, and what the admission webhook rejects. +--- + +# SecRule ID ranges + +Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. To keep one namespace from colliding with another's IDs — or disabling the cluster's baseline rules — Gateway WAF assigns each namespace its own range of IDs and enforces it through an admission webhook. + +## How a namespace's range is derived + +Each namespace is allocated a contiguous block of 10,000 IDs, derived deterministically from the namespace name: + +```text +prefix = first four hex digits of sha256(namespace name), modulo 10000 +range = [ prefix * 10000 , prefix * 10000 + 9999 ] +``` + +Because the range is a pure function of the namespace name, it never changes and requires no cluster state. Across the ID space this gives up to 10,000 namespaces a private block of 10,000 IDs each. + +## Finding your range + +On the first time a namespace references a plugin, the controller stamps the range onto the namespace as an annotation: + +```bash +kubectl get namespace payments \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +``` + +Pick the IDs for your `WAFPlugin` rules from inside this range. + +## Protected ranges + +Some IDs are reserved and rejected for every namespace: + +- **`900000`–`999999`** — the OWASP Core Rule Set baseline. +- **Tigera-shipped baseline rules** — the rules that implement the managed security baseline. + +A namespace can neither define rules with these IDs nor remove them with `SecRuleRemoveById`. This is what prevents a tenant from quietly disabling the cluster's protection. + +## What the admission webhook rejects + +When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive and rejects the resource if any directive fails one of these checks. The reason appears on the resource's `Accepted` condition. + +| Check | Rejected with | Meaning | +| :--- | :--- | :--- | +| Directive not on the allowlist | `UnsafeDirective` | The directive type isn't permitted (for example, one that reads files or fetches remote rules) | +| Rule ID outside your range | `IDOutsideRange` | A `SecRule`/`SecAction` ID falls outside the namespace's allocated range | +| Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a CRS baseline rule) | + +The controller re-runs the same checks during reconciliation as a backstop; if a resource somehow reaches reconcile with a disallowed directive, it is marked `Accepted=False` with the matching reason rather than programmed. + +## Allowed directives + +The allowlist admits the directives needed to write and tune rules — among them `SecRule`, `SecAction`, `SecDefaultAction`, `SecRuleRemoveById`, `SecRuleRemoveByTag`, `SecRuleUpdateActionById`, `SecRuleUpdateTargetById`, `SecMarker`, and the request/response body and argument limits. Directives that read from the filesystem, write audit logs to arbitrary paths, or fetch remote rules are denied. + +## Example + +A namespace whose range is `420000`–`429999` writes rules using IDs from that block: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: api-exceptions + namespace: payments +spec: + rules: | + SecRule REQUEST_BODY "@rx __schema" \ + "id:420020,phase:2,deny,status:403,msg:'Introspection blocked'" +``` diff --git a/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx b/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx new file mode 100644 index 0000000000..673d287966 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx @@ -0,0 +1,82 @@ +--- +description: Read the status conditions Gateway WAF sets on its resources — Licensed, Accepted, Programmed, Validated, and Ready — and what each reason means. +--- + +# Status conditions + +Gateway WAF reports the state of each policy through standard Kubernetes status conditions. Read them with `kubectl`: + +```bash +kubectl describe wafpolicy payments-waf -n payments +# or +kubectl get wafpolicy payments-waf -n payments -o jsonpath='{.status.conditions}' | jq +``` + +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated roughly in that order — a failure early on explains why later conditions are absent or `False`. + +## Licensed + +Gateway WAF is a $[prodname] feature and requires a valid license. + +| Reason | Meaning | +| :--- | :--- | +| `LicenseValid` | The license entitles this feature | +| `LicenseBlocked` | No valid entitlement; the policy is not programmed | + +## Accepted + +Whether the resource is structurally valid and admitted. Several reasons come straight from the [admission webhook](secrule-id-ranges.mdx#what-the-admission-webhook-rejects). + +| Reason | Meaning | +| :--- | :--- | +| `Accepted` | The resource is valid and admitted | +| `Conflicted` | Another policy already targets this route; the older one wins (the message names it) | +| `TargetNotFound` | The referenced `Gateway`/`HTTPRoute` does not exist | +| `UnsupportedTarget` | The `targetRefs` kind is not supported | +| `UnsafeDirective` | A directive is not on the allowlist | +| `IDOutsideRange` | A rule ID is outside the namespace's allocated range | +| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected baseline rule | +| `SectionNameNotYetSupported` | `sectionName` was set; per-rule attachment is reserved for a future release | + +## Validated + +The result of evaluating [validation policies](rego-validation.mdx) against the merged configuration. + +| Reason | Meaning | +| :--- | :--- | +| `Compliant` | No violations | +| `ViolationsFound` | One or more rules reported a violation (see `status.validation.failures`) | +| `NotEvaluated` | No validation policy applies to this resource | + +## Programmed + +Whether the Coraza WASM filter was successfully programmed into Envoy Gateway. + +| Reason | Meaning | +| :--- | :--- | +| `ConfigurationApplied` | The `EnvoyExtensionPolicy` was generated and the filter is active | +| `WASMUnavailable` | The Coraza WASM image could not be pulled | +| `NotAttempted` | A `Critical` violation under `Enforce` blocked programming | + +## Ready + +A roll-up of the conditions above. + +| Reason | Meaning | +| :--- | :--- | +| `Ready` | The policy is active on the data plane | +| `NotReady` | One of the conditions above is not satisfied | +| `LicenseBlocked` | Blocked on licensing | + +## Validation summary + +For convenience, `WAFPolicy.status.validation` summarizes the outcome of validation without having to read individual conditions: + +| Field | Meaning | +| :--- | :--- | +| `status` | `Valid`, `Audited`, or `Rejected` | +| `securityPosture` | `Compliant`, `Warning`, `Degraded`, or `Critical` | +| `failures[]` | Per-rule failures — policy name, rule, severity, and message | +| `lastEvaluated` | When validation last ran | + +A `GlobalWAFValidationPolicy`'s own status aggregates the violating namespaces cluster-wide, giving the cluster operator a single place to see which namespaces are out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx new file mode 100644 index 0000000000..51c6060052 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -0,0 +1,201 @@ +--- +description: Diagnose and fix common Gateway WAF problems by reading policy status conditions and working through symptom-to-fix guidance for each failure mode. +--- + +# Troubleshooting + +Most Gateway WAF problems surface directly on the policy's status conditions. Start here: + +```bash +kubectl describe wafpolicy -n +``` + +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated in roughly that order — a failure early on explains why later conditions are absent or `False`. For the full meaning of each condition and reason, see [Status conditions](./reference/status-conditions.mdx). + +--- + +## `Accepted=False, Reason=Conflicted` + +**Cause.** Two `WAFPolicy` resources in the same namespace target the same Gateway or `HTTPRoute`. $[prodname] follows Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): the policy with the oldest `creationTimestamp` wins. The newer policy is marked `Conflicted` and its directives are never merged. + +**Fix.** Identify which policies are competing: + +```bash +kubectl get wafpolicy -n -o wide +``` + +Either delete the conflicting policy, or change one policy's `targetRefs` so they no longer point at the same target. To understand why only one policy can own a target, see [Merge order and enforcement](./concepts/merge-and-enforcement.mdx). + +--- + +## `Accepted=False, Reason=UnsafeDirective | IDOutsideRange | ProtectedIDRemoval` + +**Cause.** The admission webhook rejected the SecLang in a `WAFPlugin` or `WAFPolicy` at apply time. These checks happen before the resource reaches the controller. + +| Reason | Meaning | +| :--- | :--- | +| `UnsafeDirective` | The directive type is not on the allowlist (for example, a directive that reads from the filesystem) | +| `IDOutsideRange` | A `SecRule` or `SecAction` ID falls outside the namespace's allocated 10,000-ID range | +| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected ID — a CRS baseline rule or a Tigera-managed rule | + +**Fix.** Read the rejection message on `status.conditions`: + +```bash +kubectl get wafpolicy -n \ + -o jsonpath='{.status.conditions[?(@.type=="Accepted")].message}' +``` + +For `IDOutsideRange` and `ProtectedIDRemoval`: look up your namespace's allocated ID range and use only IDs from that block: + +```bash +kubectl get namespace \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +``` + +For `UnsafeDirective`: remove or replace the disallowed directive. See [SecRule ID ranges](./reference/secrule-id-ranges.mdx) for the full allowlist and protected range details. + +--- + +## `Programmed=False, Reason=WASMUnavailable` + +**Cause.** The Coraza WASM image could not be pulled by the Envoy proxy. This is typically a registry access problem: the image reference is unreachable, or a private registry requires a pull secret that has not been configured. + +**Diagnostic.** Check the `EnvoyExtensionPolicy` status that the controller generated for the policy: + +```bash +kubectl get envoyextensionpolicy \ + -n \ + -l applicationlayer.projectcalico.org/waf-policy= \ + -o jsonpath='{.items[*].status}' | jq +``` + +Look for messages containing `could not fetch Wasm OCI image` in the Envoy proxy logs: + +```bash +kubectl logs -n tigera-gateway \ + -l gateway.envoyproxy.io/owning-gateway-namespace= \ + -c envoy | grep -i wasm +``` + +**Fix.** The operator manages the WASM image reference. For clusters in a private registry environment, wire in the appropriate pull secret through the operator configuration. Verify the node can reach the registry and that the pull secret is correctly associated with the Envoy proxy pods. + +--- + +## `Programmed=False, Reason=NotAttempted` + +**Cause.** A `GlobalWAFValidationPolicy` in `Enforce` mode evaluated the merged configuration and found a `Critical` violation. The controller did not attempt to program the Coraza filter. + +**Diagnostic.** Read the validation failures: + +```bash +kubectl get wafpolicy -n \ + -o jsonpath='{.status.validation}' | jq +``` + +The `failures[]` array shows which `GlobalWAFValidationPolicy` rule failed, with its severity and message. The `status.validation.status` field will be `Rejected`. + +**Fix.** Correct the `WAFPolicy` to satisfy the failing rule — for example, change `action: Detect` back to `action: Block` if the validation policy requires Block mode. If you believe the policy should be an exception, ask the cluster operator to adjust the `GlobalWAFValidationPolicy`. For how to read and act on validation output, see [Validate config](./how-to/validate-config.mdx) and [Status conditions](./reference/status-conditions.mdx). + +--- + +## `Accepted=False, Reason=TargetNotFound` + +**Cause.** The `targetRefs` in the `WAFPolicy` reference a `Gateway` or `HTTPRoute` that does not exist in the same namespace. + +**Fix.** Verify the target exists: + +```bash +kubectl get gateway,httproute -n +``` + +Check that the `name`, `kind`, and `group` in `targetRefs` exactly match the resource. Create the target first, then re-apply the `WAFPolicy`, or correct the reference. Once the target exists the controller will reconcile automatically. + +--- + +## Attacks not blocked + +**Cause.** One or more of the following applies: + +- `action` is `Detect` (either explicitly or inherited from `GlobalWAFPolicy`) — traffic is inspected but not blocked. +- `coreRuleSet.state` is `Disabled` — the OWASP CRS is not loaded. +- `coreRuleSet.paranoiaLevel` is lower than the paranoia floor for the triggering CRS rule — the rule does not evaluate. + +**Diagnostic.** Check the effective merged configuration. Start with the policy's conditions and spec: + +```bash +kubectl describe wafpolicy -n +``` + +Confirm the action is `Block` and CRS is `Enabled`. If the paranoia level looks correct but the attack still passes, check whether the specific CRS rule has a higher paranoia floor than the configured level — some rules only activate at paranoia 2 or 3. + +:::tip + +`kubectl waf` (coming in a future release) will render the full merged directive list so you can inspect the effective `SecRuleEngine` line and paranoia settings directly. + +::: + +Also verify the `GlobalWAFPolicy` default action — a namespace `WAFPolicy` that does not set `action` inherits it: + +```bash +kubectl get globalwafpolicy default -o jsonpath='{.spec.defaultAction}' +``` + +See [Global vs. namespaced](./concepts/global-vs-namespaced.mdx) for field inheritance rules. + +--- + +## `403` on every request / false positives + +**Cause.** The paranoia level is too high for the traffic pattern, or a specific CRS rule is matching legitimate requests (a false positive). + +**Diagnostic.** Identify which rule is firing. Check the Envoy proxy access logs in the Gateway pod: + +```bash +kubectl logs -n tigera-gateway \ + -l gateway.envoyproxy.io/owning-gateway-namespace= \ + -c envoy | grep -i "waf\|403" | tail -30 +``` + +The log lines will include the rule ID and message that triggered the block. + +**Fix.** Two options: + +1. **Lower the paranoia level** if the false positives come from a large class of higher-paranoia rules and your threat model allows it. +2. **Add a plugin exception** to suppress the specific rule ID using `SecRuleRemoveById` or to skip the rule for a known-safe target using `SecRuleUpdateTargetById`. This is the preferred approach for surgical tuning without weakening the overall posture. + +For how to write exception rules within your namespace's ID range, see [Write custom rules](./how-to/write-custom-rules.mdx). + +:::caution + +`SecRuleRemoveById` can only target rule IDs within your namespace's allocated range. You cannot remove CRS baseline rules (IDs `900000`–`999999`) or Tigera-managed rules — the admission webhook will reject the plugin with `ProtectedIDRemoval`. + +::: + +--- + +## General diagnostic sequence + +If no specific symptom matches above, walk the condition chain in order: + +```bash +kubectl get wafpolicy -n \ + -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}): {.message}{"\n"}{end}' +``` + +1. `Licensed=False` → The license is missing or does not entitle Gateway WAF. Check [Enable Gateway WAF](./get-started/enable.mdx). +2. `Accepted=False` → The policy spec is rejected. Read the `Reason` and `message` — see the sections above. +3. `Validated=False` → A validation rule fired. Read `status.validation.failures`. +4. `Programmed=False` → The filter was not loaded into Envoy. Read the `Reason` — see `WASMUnavailable` and `NotAttempted` above. +5. All conditions `True` but WAF not blocking → Allow a few seconds for Envoy to load the WASM module after `EnvoyExtensionPolicy` creation. If it still does not block, the effective action is likely `Detect` — see [Attacks not blocked](#attacks-not-blocked) above. + +## Related pages + +- [Reference: Status conditions](./reference/status-conditions.mdx) +- [Reference: SecRule ID ranges](./reference/secrule-id-ranges.mdx) +- [Reference: Rego validation](./reference/rego-validation.mdx) +- [Reference: CRDs](./reference/crds.mdx) +- [Concepts: Merge order and enforcement](./concepts/merge-and-enforcement.mdx) +- [Concepts: Global vs. namespaced](./concepts/global-vs-namespaced.mdx) +- [How-to: Attach WAF to a route](./how-to/attach-waf-to-a-route.mdx) +- [How-to: Write custom rules](./how-to/write-custom-rules.mdx) +- [How-to: Validate config](./how-to/validate-config.mdx) diff --git a/sidebars-calico-enterprise.js b/sidebars-calico-enterprise.js index aa90815e3d..7ac4f663fd 100644 --- a/sidebars-calico-enterprise.js +++ b/sidebars-calico-enterprise.js @@ -472,6 +472,52 @@ module.exports = { 'threat/deeppacketinspection', 'threat/web-application-firewall', 'threat/deploying-waf-ingress-gateway', + { + type: 'category', + label: 'Gateway WAF', + link: { type: 'doc', id: 'threat/gateway-waf/index' }, + items: [ + { + type: 'category', + label: 'Concepts', + items: [ + 'threat/gateway-waf/concepts/policies-plugins-validation', + 'threat/gateway-waf/concepts/global-vs-namespaced', + 'threat/gateway-waf/concepts/merge-and-enforcement', + ], + }, + { + type: 'category', + label: 'Get started', + items: [ + 'threat/gateway-waf/get-started/enable', + 'threat/gateway-waf/get-started/quickstart', + ], + }, + { + type: 'category', + label: 'How to', + items: [ + 'threat/gateway-waf/how-to/set-a-baseline', + 'threat/gateway-waf/how-to/attach-waf-to-a-route', + 'threat/gateway-waf/how-to/write-custom-rules', + 'threat/gateway-waf/how-to/validate-config', + 'threat/gateway-waf/how-to/multi-tenant-setup', + ], + }, + { + type: 'category', + label: 'Reference', + items: [ + 'threat/gateway-waf/reference/crds', + 'threat/gateway-waf/reference/rego-validation', + 'threat/gateway-waf/reference/secrule-id-ranges', + 'threat/gateway-waf/reference/status-conditions', + ], + }, + 'threat/gateway-waf/troubleshooting', + ], + }, ], }, { From cd13609861f77e2296c348b31ed10693371a6ccf Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 13:01:45 +0100 Subject: [PATCH 02/16] docs: fix Vale errors in Gateway WAF section - Add WAF terms to the CalicoTerminology vocab (Rego, allowlist, the six WAF CRD kinds, and targetRefs/namespaceSelector/paranoiaLevel/ coreRuleSet/defaultAction/enforcementMode/SecLang). - Reword two "another's" possessives (Vale.Spelling false positive). - "walks through" -> "walks you through" (CalicoStyle.Walkthrough). Verified with scripts/vale-lint.sh --minAlertLevel=error: 0 errors. --- .../vocabularies/CalicoTerminology/accept.txt | 17 +++++++++++++++++ .../concepts/merge-and-enforcement.mdx | 2 +- .../gateway-waf/how-to/multi-tenant-setup.mdx | 2 +- .../gateway-waf/reference/secrule-id-ranges.mdx | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/styles/config/vocabularies/CalicoTerminology/accept.txt b/.github/styles/config/vocabularies/CalicoTerminology/accept.txt index f84795dba5..5625ab0a05 100644 --- a/.github/styles/config/vocabularies/CalicoTerminology/accept.txt +++ b/.github/styles/config/vocabularies/CalicoTerminology/accept.txt @@ -144,3 +144,20 @@ VNet[s]? [dD]ecap(sulation)? [rR]epo[s]? [dD]iags + +# Gateway WAF (per-route and namespaced WAF) +[rR]ego +[aA]llowlist(ed|ing|s)? +GlobalWAFPolic(y|ies) +WAFPolic(y|ies) +GlobalWAFPlugins? +WAFPlugins? +GlobalWAFValidationPolic(y|ies) +WAFValidationPolic(y|ies) +targetRefs? +namespaceSelector +paranoiaLevel +coreRuleSet +defaultAction +enforcementMode +SecLang diff --git a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx index 00b9d156b7..aa2380cf8f 100644 --- a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx +++ b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx @@ -58,7 +58,7 @@ Before any of the above runs, a validating admission webhook checks the raw SecL | Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | | Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | -This is what stops one namespace from colliding with another's rule IDs, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived and which rule IDs are protected. +This is what stops one namespace from colliding with the rule IDs of another, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived and which rule IDs are protected. ## Validation and enforcement diff --git a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx index da4f49260c..59221c2b99 100644 --- a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx +++ b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx @@ -6,7 +6,7 @@ description: Walk through an end-to-end Gateway WAF multi-tenant setup where a c :::note[Scenario] -This guide walks through a single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. You will see the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. +This guide walks you through a single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. You will see the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. Prerequisites: Gateway WAF is enabled and a valid license is in place. See [Enable Gateway WAF](../get-started/enable.mdx). diff --git a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx index 914e3d1827..4ea81901f1 100644 --- a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx +++ b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx @@ -4,7 +4,7 @@ description: How Gateway WAF assigns each namespace a SecRule ID range, which ru # SecRule ID ranges -Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. To keep one namespace from colliding with another's IDs — or disabling the cluster's baseline rules — Gateway WAF assigns each namespace its own range of IDs and enforces it through an admission webhook. +Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. To keep one namespace from colliding with the IDs of another — or disabling the cluster's baseline rules — Gateway WAF assigns each namespace its own range of IDs and enforces it through an admission webhook. ## How a namespace's range is derived From 59940e71a1e40ba10dd86f704b47946677eb1d55 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 13:19:33 +0100 Subject: [PATCH 03/16] docs: spell out "Core Rule Set" in Gateway WAF prose The reviewdog/Vale gate fails on warnings too, and Vale.Terms flagged the bare acronym "CRS" as a miscasing of "CRs" (the custom-resources term). Replace the bare acronym in prose with "Core Rule Set"; code blocks and inline-code literals (e.g. the Rego rule messages) are left unchanged so examples stay accurate. Verified with scripts/vale-lint.sh: 0 errors, 0 warnings, 0 suggestions. --- .../concepts/merge-and-enforcement.mdx | 4 ++-- .../concepts/policies-plugins-validation.mdx | 4 ++-- .../gateway-waf/get-started/quickstart.mdx | 2 +- .../how-to/attach-waf-to-a-route.mdx | 2 +- .../gateway-waf/how-to/multi-tenant-setup.mdx | 8 +++---- .../gateway-waf/how-to/write-custom-rules.mdx | 22 +++++++++---------- .../threat/gateway-waf/index.mdx | 4 ++-- .../threat/gateway-waf/reference/crds.mdx | 8 +++---- .../reference/secrule-id-ranges.mdx | 2 +- .../threat/gateway-waf/troubleshooting.mdx | 12 +++++----- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx index aa2380cf8f..ed116cf26a 100644 --- a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx +++ b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx @@ -20,12 +20,12 @@ Directive order is not cosmetic — the Core Rule Set requires configuration var 3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. 4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. -**CRS setup** +**Core Rule Set setup** 5. Core Rule Set setup — reads the `tx.*` variables set above. 6. Blocking paranoia level, from the merged paranoia level. 7. Detection paranoia level (always ≥ blocking level). -**Pre-CRS rules (`before`)** +**Pre-Core Rule Set rules (`before`)** 8. Global plugin `before` rules. 9. Namespace plugin `before` rules. diff --git a/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx b/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx index 75df354b6b..38add99b2c 100644 --- a/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx +++ b/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx @@ -8,7 +8,7 @@ Gateway WAF is configured through six custom resources in the `applicationlayer. | Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | | :--- | :--- | :--- | :--- | -| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, CRS state, paranoia, which plugins | +| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, Core Rule Set state, paranoia, which plugins | | **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | | **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | @@ -23,7 +23,7 @@ Reach for a policy when you want to **turn WAF on for some traffic** and set its ## Plugins — custom rules -A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [CRS 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. +A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. - `GlobalWAFPlugin` holds organization-wide rules — security baselines, compliance rules. - `WAFPlugin` holds application-specific rules and false-positive exceptions. diff --git a/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx b/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx index 6d7dcf94da..42b967d641 100644 --- a/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx +++ b/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx @@ -249,7 +249,7 @@ kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl 403 ``` -The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. CRS rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. +The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. ## Clean up diff --git a/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx b/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx index 8f22342767..23f167c919 100644 --- a/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx +++ b/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx @@ -145,7 +145,7 @@ Fields you set in `WAFPolicy` override the corresponding field in `GlobalWAFPoli | `WAFPolicy` field | When set | When unset | | :--- | :--- | :--- | | `action` | overrides `GlobalWAFPolicy.spec.defaultAction` | inherits the global default | -| `coreRuleSet.state` | overrides the global CRS state | inherits the global CRS state | +| `coreRuleSet.state` | overrides the global Core Rule Set state | inherits the global Core Rule Set state | | `coreRuleSet.paranoiaLevel` | overrides the global paranoia level | inherits the global paranoia level | For the complete picture, including how global and namespace plugins stack, see [Global vs. namespaced](../concepts/global-vs-namespaced.mdx). diff --git a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx index 59221c2b99..8dfbeaa5eb 100644 --- a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx +++ b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx @@ -36,7 +36,7 @@ spec: - name: org-security-baseline ``` -This establishes Block mode and OWASP CRS at paranoia level 1 for every namespace. A `WAFPolicy` can raise the paranoia level or override the action, but the validation policy below prevents it from lowering either. +This establishes Block mode and OWASP Core Rule Set at paranoia level 1 for every namespace. A `WAFPolicy` can raise the paranoia level or override the action, but the validation policy below prevents it from lowering either. ### Add an org-wide plugin @@ -177,7 +177,7 @@ spec: After reconciliation the policy should reach `Ready=True`. The merged ruleset includes: 1. The org-wide `X-Request-ID` requirement from `GlobalWAFPlugin/org-security-baseline` -2. The OWASP CRS at paranoia level 2 (overriding the global level 1) +2. The OWASP Core Rule Set at paranoia level 2 (overriding the global level 1) 3. The `after` audit rule from the global plugin Verify the conditions: @@ -264,8 +264,8 @@ No `EnvoyExtensionPolicy` is generated until the policy is corrected. The `payme Each team's WAF configuration is fully isolated by namespace: -- **`team-a`** runs CRS at paranoia 2 with its own custom rules, scoped to its ID range. -- **`payments`** runs CRS at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. +- **`team-a`** runs Core Rule Set at paranoia 2 with its own custom rules, scoped to its ID range. +- **`payments`** runs Core Rule Set at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. - Neither team can disable the org-wide `X-Request-ID` rule or lower below Block mode. - The cluster operator sees all validation violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — a single place to audit which namespaces are out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx index d0634ba61b..8dac9dc5b4 100644 --- a/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx @@ -10,7 +10,7 @@ This page is for **application operators** who manage WAF configuration within a ::: -A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots. The slots map to CRS 4's plugin file structure and determine when your directives run relative to the OWASP Core Rule Set. +A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots. The slots map to Core Rule Set 4's plugin file structure and determine when your directives run relative to the OWASP Core Rule Set. ## The four slots @@ -18,7 +18,7 @@ The merger assembles every plugin's slots into a fixed load order. Understanding ### `config` — variable setup -**Runs before CRS setup.** Use `config` to set `tx.*` transaction variables that the CRS reads during its own initialization. Examples: tuning anomaly score thresholds, enabling optional CRS features, or setting application-specific flags. +**Runs before Core Rule Set setup.** Use `config` to set `tx.*` transaction variables that the Core Rule Set reads during its own initialization. Examples: tuning anomaly score thresholds, enabling optional Core Rule Set features, or setting application-specific flags. ```yaml config: | @@ -30,9 +30,9 @@ config: | If you write a rule in `config` that fires on a request variable (such as a request header), place it in `before` instead — at `config` time the request has not been read yet. -### `before` — pre-CRS rules +### `before` — pre-Core Rule Set rules -**Runs after variable setup but before the CRS loads.** Use `before` for rules that should execute before CRS analysis: early-exit allowlisting for trusted paths, request normalization, or setting flags that later CRS rules or your own rules can read. +**Runs after variable setup but before the Core Rule Set loads.** Use `before` for rules that should execute before Core Rule Set analysis: early-exit allowlisting for trusted paths, request normalization, or setting flags that later Core Rule Set rules or your own rules can read. ```yaml before: | @@ -46,9 +46,9 @@ before: | ::: -### `rules` — custom rules alongside CRS +### `rules` — custom rules alongside Core Rule Set -**Runs after the full CRS has executed.** This is a Tigera extension to the CRS plugin model. Use `rules` for application-specific detections — rules that complement the CRS rather than pre-empting it. Because they run after the CRS, they can also read and act on anomaly scores. +**Runs after the full Core Rule Set has executed.** This is a Tigera extension to the Core Rule Set plugin model. Use `rules` for application-specific detections — rules that complement the Core Rule Set rather than pre-empting it. Because they run after the Core Rule Set, they can also read and act on anomaly scores. ```yaml rules: | @@ -58,7 +58,7 @@ rules: | logdata:'URI=%{REQUEST_URI}'" ``` -### `after` — post-CRS processing +### `after` — post-Core Rule Set processing **Runs last.** Use `after` for false-positive suppression, response inspection, or logging augmentation. Because `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, the cluster operator has the final say on any post-processing — see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#the-merged-ruleset-load-order). @@ -69,7 +69,7 @@ after: | :::note -`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected CRS baseline rules regardless of slot — the admission webhook will reject the attempt. +`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected Core Rule Set baseline rules regardless of slot — the admission webhook will reject the attempt. ::: @@ -88,7 +88,7 @@ Pick all your rule IDs from inside this range. The annotation is populated the f The admission webhook rejects resources with: - Rule IDs outside your allocated range (`IDOutsideRange`) -- `SecRuleRemoveById` targeting a protected CRS baseline rule (`ProtectedIDRemoval`) +- `SecRuleRemoveById` targeting a protected Core Rule Set baseline rule (`ProtectedIDRemoval`) - Directives not on the allowlist (`UnsafeDirective`) See [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for protected ranges, the derivation algorithm, and the full allowlist. @@ -170,9 +170,9 @@ spec: If your policy also references a `GlobalWAFPlugin`, the global plugin's slots load before the namespace plugin's slots at each position in the merge order. See [Attach WAF to a route](./attach-waf-to-a-route.mdx) for full `targetRefs` options. -## CRS and SecLang references +## Core Rule Set and SecLang references -Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The CRS documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. +Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The Core Rule Set documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. :::note diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx index 4e008f1261..425fab1186 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -21,7 +21,7 @@ Protect the traffic entering your cluster through $[prodname] Ingress Gateway wi Edge WAFs traditionally apply one ruleset to everything behind the gateway. Gateway WAF takes a Kubernetes-native, multi-tenant approach: a cluster operator sets a security baseline once, and each application team layers its own rules and exceptions on top — scoped to their own Gateways and `HTTPRoute`s, within guardrails the cluster operator defines. -Gateway WAF runs the industry-standard [OWASP ModSecurity Core Rule Set (CRS) v4](https://owasp.org/www-project-modsecurity-core-rule-set/) as a [Coraza](https://coraza.io/) WebAssembly filter inside Envoy Gateway — no sidecars, and no traffic ever leaves the proxy for inspection. +Gateway WAF runs the industry-standard [OWASP ModSecurity Core Rule Set v4](https://owasp.org/www-project-modsecurity-core-rule-set/) as a [Coraza](https://coraza.io/) WebAssembly filter inside Envoy Gateway — no sidecars, and no traffic ever leaves the proxy for inspection. ## How it works @@ -50,7 +50,7 @@ Gateway WAF splits cleanly across two personas. Cluster-scoped resources belong | Task | Cluster operator (CO) | Application operator (AO) | | :--- | :--- | :--- | | Enable the feature (license + `GatewayAPI` gate) | ✓ | | -| Cluster-wide baseline — action, CRS, paranoia | `GlobalWAFPolicy` (singleton) | | +| Cluster-wide baseline — action, Core Rule Set, paranoia | `GlobalWAFPolicy` (singleton) | | | Organization-wide custom rules | `GlobalWAFPlugin` | | | Compliance guardrails (audit or enforce) | `GlobalWAFValidationPolicy` | | | Attach WAF to a Gateway or `HTTPRoute` | | `WAFPolicy` (`targetRefs`) | diff --git a/calico-enterprise/threat/gateway-waf/reference/crds.mdx b/calico-enterprise/threat/gateway-waf/reference/crds.mdx index 0784715c74..e51e95bffb 100644 --- a/calico-enterprise/threat/gateway-waf/reference/crds.mdx +++ b/calico-enterprise/threat/gateway-waf/reference/crds.mdx @@ -135,10 +135,10 @@ spec: | Field | Type | Notes | | :--- | :--- | :--- | | `description` | string | Human-readable purpose; max 1024 chars | -| `config` | string (SecLang) | Slot 1 — `tx.*` variable setup, runs before CRS setup; max 65536 chars | -| `before` | string (SecLang) | Slot 2 — pre-CRS rules; max 65536 chars | -| `rules` | string (SecLang) | Slot 3 — custom rules alongside CRS (Tigera extension); max 65536 chars | -| `after` | string (SecLang) | Slot 4 — post-CRS rules; max 65536 chars | +| `config` | string (SecLang) | Slot 1 — `tx.*` variable setup, runs before Core Rule Set setup; max 65536 chars | +| `before` | string (SecLang) | Slot 2 — pre-Core Rule Set rules; max 65536 chars | +| `rules` | string (SecLang) | Slot 3 — custom rules alongside Core Rule Set (Tigera extension); max 65536 chars | +| `after` | string (SecLang) | Slot 4 — post-Core Rule Set rules; max 65536 chars | Rule IDs must fall in your namespace's allocated range — see [SecRule ID ranges](secrule-id-ranges.mdx). diff --git a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx index 4ea81901f1..6b8824997b 100644 --- a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx +++ b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx @@ -45,7 +45,7 @@ When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive | :--- | :--- | :--- | | Directive not on the allowlist | `UnsafeDirective` | The directive type isn't permitted (for example, one that reads files or fetches remote rules) | | Rule ID outside your range | `IDOutsideRange` | A `SecRule`/`SecAction` ID falls outside the namespace's allocated range | -| Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a CRS baseline rule) | +| Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a Core Rule Set baseline rule) | The controller re-runs the same checks during reconciliation as a backstop; if a resource somehow reaches reconcile with a disallowed directive, it is marked `Accepted=False` with the matching reason rather than programmed. diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index 51c6060052..f04bdb6a47 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -36,7 +36,7 @@ Either delete the conflicting policy, or change one policy's `targetRefs` so the | :--- | :--- | | `UnsafeDirective` | The directive type is not on the allowlist (for example, a directive that reads from the filesystem) | | `IDOutsideRange` | A `SecRule` or `SecAction` ID falls outside the namespace's allocated 10,000-ID range | -| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected ID — a CRS baseline rule or a Tigera-managed rule | +| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected ID — a Core Rule Set baseline rule or a Tigera-managed rule | **Fix.** Read the rejection message on `status.conditions`: @@ -117,8 +117,8 @@ Check that the `name`, `kind`, and `group` in `targetRefs` exactly match the res **Cause.** One or more of the following applies: - `action` is `Detect` (either explicitly or inherited from `GlobalWAFPolicy`) — traffic is inspected but not blocked. -- `coreRuleSet.state` is `Disabled` — the OWASP CRS is not loaded. -- `coreRuleSet.paranoiaLevel` is lower than the paranoia floor for the triggering CRS rule — the rule does not evaluate. +- `coreRuleSet.state` is `Disabled` — the OWASP Core Rule Set is not loaded. +- `coreRuleSet.paranoiaLevel` is lower than the paranoia floor for the triggering Core Rule Set rule — the rule does not evaluate. **Diagnostic.** Check the effective merged configuration. Start with the policy's conditions and spec: @@ -126,7 +126,7 @@ Check that the `name`, `kind`, and `group` in `targetRefs` exactly match the res kubectl describe wafpolicy -n ``` -Confirm the action is `Block` and CRS is `Enabled`. If the paranoia level looks correct but the attack still passes, check whether the specific CRS rule has a higher paranoia floor than the configured level — some rules only activate at paranoia 2 or 3. +Confirm the action is `Block` and Core Rule Set is `Enabled`. If the paranoia level looks correct but the attack still passes, check whether the specific Core Rule Set rule has a higher paranoia floor than the configured level — some rules only activate at paranoia 2 or 3. :::tip @@ -146,7 +146,7 @@ See [Global vs. namespaced](./concepts/global-vs-namespaced.mdx) for field inher ## `403` on every request / false positives -**Cause.** The paranoia level is too high for the traffic pattern, or a specific CRS rule is matching legitimate requests (a false positive). +**Cause.** The paranoia level is too high for the traffic pattern, or a specific Core Rule Set rule is matching legitimate requests (a false positive). **Diagnostic.** Identify which rule is firing. Check the Envoy proxy access logs in the Gateway pod: @@ -167,7 +167,7 @@ For how to write exception rules within your namespace's ID range, see [Write cu :::caution -`SecRuleRemoveById` can only target rule IDs within your namespace's allocated range. You cannot remove CRS baseline rules (IDs `900000`–`999999`) or Tigera-managed rules — the admission webhook will reject the plugin with `ProtectedIDRemoval`. +`SecRuleRemoveById` can only target rule IDs within your namespace's allocated range. You cannot remove Core Rule Set baseline rules (IDs `900000`–`999999`) or Tigera-managed rules — the admission webhook will reject the plugin with `ProtectedIDRemoval`. ::: From cb00f3ae25cd170429ba11790e322b638325da4d Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 13:44:11 +0100 Subject: [PATCH 04/16] docs: render Gateway WAF architecture diagram with Mermaid Replace the ASCII architecture diagram in the Gateway WAF overview with a Mermaid flowchart, and introduce @docusaurus/theme-mermaid to the repo (first Mermaid usage here). The repo builds with future.faster.ssgWorkerThreads, under which theme-mermaid's context hooks (useColorMode/useThemeConfig) throw during worker-thread SSG. So the diagram is rendered client-side via + @theme/Mermaid, and markdown.mermaid is intentionally NOT enabled (it would auto-transform ```mermaid fences and crash SSG repo-wide). Verified BUILD_NEXT build: the gateway-waf pages render cleanly; the only build failure is pre-existing broken links in unrelated calico/calico-cloud next docs. --- .../threat/gateway-waf/index.mdx | 40 +- docusaurus.config.js | 1 + package.json | 1 + yarn.lock | 1016 ++++++++++++++++- 4 files changed, 1035 insertions(+), 23 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx index 425fab1186..b4d79fa80b 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -4,6 +4,8 @@ hide_table_of_contents: true --- import { DocCardLink, DocCardLinkLayout } from '/src/___new___/components'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import Mermaid from '@theme/Mermaid'; # Gateway WAF @@ -27,21 +29,29 @@ Gateway WAF runs the industry-standard [OWASP ModSecurity Core Rule Set v4](http Gateway WAF is configured entirely through Kubernetes resources. The controller merges every policy and plugin that applies to a target, generates an ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy` that loads the Coraza WASM filter. -```text - write time reconcile time data plane - ┌────────────────────┐ ┌─────────────────────────────────┐ ┌──────────────────────┐ - │ admission webhook │ │ merge ─▶ ordered SecLang ─▶ │ pass │ EnvoyExtensionPolicy │ - │ (allowlist + per-ns │ ──apply─▶│ Rego validation │ ─────▶ │ (Coraza WASM filter) │ ─▶ request - │ SecRule ID range) │ ok │ │ │ on Envoy Gateway │ - └────────────────────┘ └─────────────────────────────────┘ └──────────────────────┘ - │ fail (Enforce + Critical) - ▼ - no filter programmed - (status reports the violation) - - inputs to merge: GlobalWAFPolicy + WAFPolicy · GlobalWAFPlugin + WAFPlugin - guardrails: admission webhook (structural) · validation policies (compliance, Rego) -``` + + {() => ( + directive allowlist +
per-namespace SecRule ID range"] + end + subgraph rt["Reconcile time"] + M["Merge policies + plugins"] --> DIR["Ordered SecLang directives"] + DIR --> VAL{"Rego validation"} + end + subgraph dp["Data plane"] + EEP["EnvoyExtensionPolicy
Coraza WASM filter"] --> EG["Envoy Gateway"] --> REQ["Request inspected"] + end + W -->|apply accepted| M + VAL -->|pass| EEP + VAL -->|"Enforce + Critical: fail"| REJ["No filter programmed
status reports the violation"] +`} + /> + )} +
+ +The inputs to the merge are `GlobalWAFPolicy` + `WAFPolicy` and `GlobalWAFPlugin` + `WAFPlugin`. Two guardrails apply: the admission webhook (structural, at write time) and validation policies (compliance, after the merge). ## Who does what diff --git a/docusaurus.config.js b/docusaurus.config.js index 1bbdc541bb..919772cc2e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -45,6 +45,7 @@ export default async function createAsyncConfig() { onBrokenMarkdownLinks: 'throw', }, }, + themes: ['@docusaurus/theme-mermaid'], // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want diff --git a/package.json b/package.json index 9bf70d216b..f01298b769 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@docusaurus/plugin-content-docs": "^3.10.1", "@docusaurus/preset-classic": "^3.10.1", "@docusaurus/theme-common": "^3.10.1", + "@docusaurus/theme-mermaid": "^3.10.1", "@docusaurus/theme-search-algolia": "^3.10.1", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", diff --git a/yarn.lock b/yarn.lock index a1954dc61c..fd980d83fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -430,6 +430,16 @@ __metadata: languageName: node linkType: hard +"@antfu/install-pkg@npm:^1.1.0": + version: 1.1.0 + resolution: "@antfu/install-pkg@npm:1.1.0" + dependencies: + package-manager-detector: "npm:^1.3.0" + tinyexec: "npm:^1.0.1" + checksum: 10c0/140d5994c76fd3d0e824c88f1ce91b3370e8066a8bc2f5729ae133bf768caa239f7915e29c78f239b7ead253113ace51293e95127fafe2b786b88eb615b3be47 + languageName: node + linkType: hard + "@apify/consts@npm:^2.20.0, @apify/consts@npm:^2.43.0": version: 2.43.0 resolution: "@apify/consts@npm:2.43.0" @@ -2056,6 +2066,13 @@ __metadata: languageName: node linkType: hard +"@braintree/sanitize-url@npm:^7.1.1": + version: 7.1.2 + resolution: "@braintree/sanitize-url@npm:7.1.2" + checksum: 10c0/62f2aa0cf58626e3880b2dc1025c42064b4639abd157ae4e1c35f4c2f5031e9273772046a423979845069c814e27ff818e8e669280dc53585e6f033d5b7a59cb + languageName: node + linkType: hard + "@chakra-ui/anatomy@npm:2.2.2": version: 2.2.2 resolution: "@chakra-ui/anatomy@npm:2.2.2" @@ -2360,6 +2377,13 @@ __metadata: languageName: node linkType: hard +"@chevrotain/types@npm:~11.1.1": + version: 11.1.2 + resolution: "@chevrotain/types@npm:11.1.2" + checksum: 10c0/c0c4679a3d407df34e18d5adfa7ac599b4a2bfddbf68da6e43678b9b3e16ab911de7766b37b9fc466261c3dead3db1b620e2e344f800fa9f0f381720475eda8f + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -3763,6 +3787,28 @@ __metadata: languageName: node linkType: hard +"@docusaurus/theme-mermaid@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-mermaid@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/module-type-aliases": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" + mermaid: "npm:>=11.6.0" + tslib: "npm:^2.6.0" + peerDependencies: + "@mermaid-js/layout-elk": ^0.1.9 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@mermaid-js/layout-elk": + optional: true + checksum: 10c0/73839669eb3e69ec565a16853b6a8fa636f3fd94d6656976835a0304bb6e7402b5dcbd7808b8b158bb0f34e61fee1d835511d88f59f48fa613eb51532614992c + languageName: node + linkType: hard + "@docusaurus/theme-search-algolia@npm:3.10.1, @docusaurus/theme-search-algolia@npm:^3.10.1": version: 3.10.1 resolution: "@docusaurus/theme-search-algolia@npm:3.10.1" @@ -4118,6 +4164,24 @@ __metadata: languageName: node linkType: hard +"@iconify/types@npm:^2.0.0": + version: 2.0.0 + resolution: "@iconify/types@npm:2.0.0" + checksum: 10c0/65a3be43500c7ccacf360e136d00e1717f050b7b91da644e94370256ac66f582d59212bdb30d00788aab4fc078262e91c95b805d1808d654b72f6d2072a7e4b2 + languageName: node + linkType: hard + +"@iconify/utils@npm:^3.0.2": + version: 3.1.3 + resolution: "@iconify/utils@npm:3.1.3" + dependencies: + "@antfu/install-pkg": "npm:^1.1.0" + "@iconify/types": "npm:^2.0.0" + import-meta-resolve: "npm:^4.2.0" + checksum: 10c0/6d01196172ef062a9cd0ee299dff3a405221e65057d6cca9ac9fb4bacd7d78168d54b0837b75d5f163038d770a3a7e4449d68ea2fc7a577c01b9ab51bfd53c98 + languageName: node + linkType: hard + "@inquirer/figures@npm:^1.0.3": version: 1.0.12 resolution: "@inquirer/figures@npm:1.0.12" @@ -4730,6 +4794,15 @@ __metadata: languageName: node linkType: hard +"@mermaid-js/parser@npm:^1.1.1": + version: 1.1.1 + resolution: "@mermaid-js/parser@npm:1.1.1" + dependencies: + "@chevrotain/types": "npm:~11.1.1" + checksum: 10c0/66414d9d66f3a86a6751427cb8890a00beaaee2b7eba34c21b34b60cdaf9237d21e9b50b9bf1f560a01a600ee332d33b949fd8f12cbda3238aba50edc0dfdf15 + languageName: node + linkType: hard + "@module-federation/error-codes@npm:0.22.0": version: 0.22.0 resolution: "@module-federation/error-codes@npm:0.22.0" @@ -6760,6 +6833,278 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.2.2 + resolution: "@types/d3-array@npm:3.2.2" + checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028 + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.6 + resolution: "@types/d3-axis@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.6 + resolution: "@types/d3-brush@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.6 + resolution: "@types/d3-chord@npm:3.0.6" + checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae + languageName: node + linkType: hard + +"@types/d3-contour@npm:*": + version: 3.0.6 + resolution: "@types/d3-contour@npm:3.0.6" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.4 + resolution: "@types/d3-delaunay@npm:6.0.4" + checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.7 + resolution: "@types/d3-dispatch@npm:3.0.7" + checksum: 10c0/38c6605ebf0bf0099dfb70eafe0dd4ae8213368b40b8f930b72a909ff2e7259d2bd8a54d100bb5a44eb4b36f4f2a62dcb37f8be59613ca6b507c7a2f910b3145 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d + languageName: node + linkType: hard + +"@types/d3-dsv@npm:*": + version: 3.0.7 + resolution: "@types/d3-dsv@npm:3.0.7" + checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.7 + resolution: "@types/d3-fetch@npm:3.0.7" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.10 + resolution: "@types/d3-force@npm:3.0.10" + checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.1.0 + resolution: "@types/d3-geo@npm:3.1.0" + dependencies: + "@types/geojson": "npm:*" + checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*": + version: 3.1.7 + resolution: "@types/d3-hierarchy@npm:3.1.7" + checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 10c0/066ebb8da570b518dd332df6b12ae3b1eaa0a7f4f0c702e3c57f812cf529cc3500ec2aac8dc094f31897790346c6b1ebd8cd7a077176727f4860c2b181a65ca4 + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.1.1 + resolution: "@types/d3-path@npm:3.1.1" + checksum: 10c0/2c36eb31ebaf2ce4712e793fd88087117976f7c4ed69cc2431825f999c8c77cca5cea286f3326432b770739ac6ccd5d04d851eb65e7a4dbcc10c982b49ad2c02 + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.2 + resolution: "@types/d3-polygon@npm:3.0.2" + checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.6 + resolution: "@types/d3-quadtree@npm:3.0.6" + checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.3 + resolution: "@types/d3-random@npm:3.0.3" + checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.1.0 + resolution: "@types/d3-scale-chromatic@npm:3.1.0" + checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.9 + resolution: "@types/d3-scale@npm:4.0.9" + dependencies: + "@types/d3-time": "npm:*" + checksum: 10c0/4ac44233c05cd50b65b33ecb35d99fdf07566bcdbc55bc1306b2f27d1c5134d8c560d356f2c8e76b096e9125ffb8d26d95f78d56e210d1c542cb255bdf31d6c8 + languageName: node + linkType: hard + +"@types/d3-selection@npm:*": + version: 3.0.11 + resolution: "@types/d3-selection@npm:3.0.11" + checksum: 10c0/0c512956c7503ff5def4bb32e0c568cc757b9a2cc400a104fc0f4cfe5e56d83ebde2a97821b6f2cb26a7148079d3b86a2f28e11d68324ed311cf35c2ed980d1d + languageName: node + linkType: hard + +"@types/d3-shape@npm:*": + version: 3.1.8 + resolution: "@types/d3-shape@npm:3.1.8" + dependencies: + "@types/d3-path": "npm:*" + checksum: 10c0/49ec2172b9eb66fc1d036e2a23966216bb972e9af51ddbed134df24bd71aedf207bb1ef81903a119eb4e1f5e927cf44beacaf64c9af86474e5548594b102b574 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.4 + resolution: "@types/d3-time@npm:3.0.4" + checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*": + version: 3.0.9 + resolution: "@types/d3-transition@npm:3.0.9" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/4f68b9df7ac745b3491216c54203cbbfa0f117ae4c60e2609cdef2db963582152035407fdff995b10ee383bae2f05b7743493f48e1b8e46df54faa836a8fb7b5 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:*": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547 + languageName: node + linkType: hard + +"@types/d3@npm:^7.4.3": + version: 7.4.3 + resolution: "@types/d3@npm:7.4.3" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -6864,6 +7209,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.16 + resolution: "@types/geojson@npm:7946.0.16" + checksum: 10c0/1ff24a288bd5860b766b073ead337d31d73bdc715e5b50a2cee5cb0af57a1ed02cc04ef295f5fa68dc40fe3e4f104dd31282b2b818a5ba3231bc1001ba084e3c + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -7485,6 +7837,21 @@ __metadata: languageName: node linkType: hard +"@upsetjs/venn.js@npm:^2.0.0": + version: 2.0.0 + resolution: "@upsetjs/venn.js@npm:2.0.0" + dependencies: + d3-selection: "npm:^3.0.0" + d3-transition: "npm:^3.0.1" + dependenciesMeta: + d3-selection: + optional: true + d3-transition: + optional: true + checksum: 10c0/b12014d94708ab4df7f5a4b6205c6f23ff235cca2ffe91df3314862b109b826e52f9020c2a2f7527d3712d21c578d6db9cdb60ce46a528739cc18e58d111f724 + languageName: node + linkType: hard + "@vladfrangu/async_event_emitter@npm:^2.2.2": version: 2.4.6 resolution: "@vladfrangu/async_event_emitter@npm:2.4.6" @@ -9180,6 +9547,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:7, commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + "commander@npm:^10.0.0": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -9201,13 +9575,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^7.2.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a - languageName: node - linkType: hard - "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -9403,6 +9770,24 @@ __metadata: languageName: node linkType: hard +"cose-base@npm:^1.0.0": + version: 1.0.3 + resolution: "cose-base@npm:1.0.3" + dependencies: + layout-base: "npm:^1.0.0" + checksum: 10c0/a6e400b1d101393d6af0967c1353355777c1106c40417c5acaef6ca8bdda41e2fc9398f466d6c85be30290943ad631f2590569f67b3fd5368a0d8318946bd24f + languageName: node + linkType: hard + +"cose-base@npm:^2.2.0": + version: 2.2.0 + resolution: "cose-base@npm:2.2.0" + dependencies: + layout-base: "npm:^2.0.0" + checksum: 10c0/14b9f8100ac322a00777ffb1daeb3321af368bbc9cabe3103943361273baee2003202ffe38e4ab770960b600214224e9c196195a78d589521540aa694df7cdec + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -9796,6 +10181,398 @@ __metadata: languageName: node linkType: hard +"cytoscape-cose-bilkent@npm:^4.1.0": + version: 4.1.0 + resolution: "cytoscape-cose-bilkent@npm:4.1.0" + dependencies: + cose-base: "npm:^1.0.0" + peerDependencies: + cytoscape: ^3.2.0 + checksum: 10c0/5e2480ddba9da1a68e700ed2c674cbfd51e9efdbd55788f1971a68de4eb30708e3b3a5e808bf5628f7a258680406bbe6586d87a9133e02a9bdc1ab1a92f512f2 + languageName: node + linkType: hard + +"cytoscape-fcose@npm:^2.2.0": + version: 2.2.0 + resolution: "cytoscape-fcose@npm:2.2.0" + dependencies: + cose-base: "npm:^2.2.0" + peerDependencies: + cytoscape: ^3.2.0 + checksum: 10c0/ce472c9f85b9057e75c5685396f8e1f2468895e71b184913e05ad56dcf3092618fe59a1054f29cb0995051ba8ebe566ad0dd49a58d62845145624bd60cd44917 + languageName: node + linkType: hard + +"cytoscape@npm:^3.33.1": + version: 3.33.4 + resolution: "cytoscape@npm:3.33.4" + checksum: 10c0/a5bd5dfd7c0f0a0e646407a4b0671fdcf45014a637d72f041bc80f51befca49477559f3ad5e4a8cf362196d079c22ae2f890632ecf17ad64f2934ce59848ebe7 + languageName: node + linkType: hard + +"d3-array@npm:1 - 2": + version: 2.12.1 + resolution: "d3-array@npm:2.12.1" + dependencies: + internmap: "npm:^1.0.0" + checksum: 10c0/7eca10427a9f113a4ca6a0f7301127cab26043fd5e362631ef5a0edd1c4b2dd70c56ed317566700c31e4a6d88b55f3951aaba192291817f243b730cb2352882e + languageName: node + linkType: hard + +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: "npm:1 - 2" + checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50 + languageName: node + linkType: hard + +"d3-axis@npm:3": + version: 3.0.0 + resolution: "d3-axis@npm:3.0.0" + checksum: 10c0/a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78 + languageName: node + linkType: hard + +"d3-brush@npm:3": + version: 3.0.0 + resolution: "d3-brush@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:3" + d3-transition: "npm:3" + checksum: 10c0/07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8 + languageName: node + linkType: hard + +"d3-chord@npm:3": + version: 3.0.1 + resolution: "d3-chord@npm:3.0.1" + dependencies: + d3-path: "npm:1 - 3" + checksum: 10c0/baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce + languageName: node + linkType: hard + +"d3-color@npm:1 - 3, d3-color@npm:3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c + languageName: node + linkType: hard + +"d3-contour@npm:4": + version: 4.0.2 + resolution: "d3-contour@npm:4.0.2" + dependencies: + d3-array: "npm:^3.2.0" + checksum: 10c0/98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032 + languageName: node + linkType: hard + +"d3-delaunay@npm:6": + version: 6.0.4 + resolution: "d3-delaunay@npm:6.0.4" + dependencies: + delaunator: "npm:5" + checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:3": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa + languageName: node + linkType: hard + +"d3-dsv@npm:1 - 3, d3-dsv@npm:3": + version: 3.0.1 + resolution: "d3-dsv@npm:3.0.1" + dependencies: + commander: "npm:7" + iconv-lite: "npm:0.6" + rw: "npm:1" + bin: + csv2json: bin/dsv2json.js + csv2tsv: bin/dsv2dsv.js + dsv2dsv: bin/dsv2dsv.js + dsv2json: bin/dsv2json.js + json2csv: bin/json2dsv.js + json2dsv: bin/json2dsv.js + json2tsv: bin/json2dsv.js + tsv2csv: bin/dsv2dsv.js + tsv2json: bin/dsv2json.js + checksum: 10c0/10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3, d3-ease@npm:3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 + languageName: node + linkType: hard + +"d3-fetch@npm:3": + version: 3.0.1 + resolution: "d3-fetch@npm:3.0.1" + dependencies: + d3-dsv: "npm:1 - 3" + checksum: 10c0/4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b + languageName: node + linkType: hard + +"d3-force@npm:3": + version: 3.0.0 + resolution: "d3-force@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-quadtree: "npm:1 - 3" + d3-timer: "npm:1 - 3" + checksum: 10c0/220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf + languageName: node + linkType: hard + +"d3-format@npm:1 - 3, d3-format@npm:3": + version: 3.1.2 + resolution: "d3-format@npm:3.1.2" + checksum: 10c0/0de452ae07585238e7f01607a7e0066665c34609652188b6ac7dc9f424f69465a425e07d16d79bd0e5955202ac7f241c66d0c76f68a79fc6f4857c94cf420652 + languageName: node + linkType: hard + +"d3-geo@npm:3": + version: 3.1.1 + resolution: "d3-geo@npm:3.1.1" + dependencies: + d3-array: "npm:2.5.0 - 3" + checksum: 10c0/d32270dd2dc8ac3ea63e8805d63239c4c8ec6c0d339d73b5e5a30a87f8f54db22a78fb434369799465eae169503b25f9a107c642c8a16c32a3285bc0e6d8e8c1 + languageName: node + linkType: hard + +"d3-hierarchy@npm:3": + version: 3.1.2 + resolution: "d3-hierarchy@npm:3.1.2" + checksum: 10c0/6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 10c0/19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a + languageName: node + linkType: hard + +"d3-path@npm:1": + version: 1.0.9 + resolution: "d3-path@npm:1.0.9" + checksum: 10c0/e35e84df5abc18091f585725b8235e1fa97efc287571585427d3a3597301e6c506dea56b11dfb3c06ca5858b3eb7f02c1bf4f6a716aa9eade01c41b92d497eb5 + languageName: node + linkType: hard + +"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da + languageName: node + linkType: hard + +"d3-polygon@npm:3": + version: 3.0.1 + resolution: "d3-polygon@npm:3.0.1" + checksum: 10c0/e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df + languageName: node + linkType: hard + +"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": + version: 3.0.1 + resolution: "d3-quadtree@npm:3.0.1" + checksum: 10c0/18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090 + languageName: node + linkType: hard + +"d3-random@npm:3": + version: 3.0.1 + resolution: "d3-random@npm:3.0.1" + checksum: 10c0/987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41 + languageName: node + linkType: hard + +"d3-sankey@npm:^0.12.3": + version: 0.12.3 + resolution: "d3-sankey@npm:0.12.3" + dependencies: + d3-array: "npm:1 - 2" + d3-shape: "npm:^1.2.0" + checksum: 10c0/261debb01a13269f6fc53b9ebaef174a015d5ad646242c23995bf514498829ab8b8f920a7873724a7494288b46bea3ce7ebc5a920b745bc8ae4caa5885cf5204 + languageName: node + linkType: hard + +"d3-scale-chromatic@npm:3": + version: 3.1.0 + resolution: "d3-scale-chromatic@npm:3.1.0" + dependencies: + d3-color: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + checksum: 10c0/9a3f4671ab0b971f4a411b42180d7cf92bfe8e8584e637ce7e698d705e18d6d38efbd20ec64f60cc0dfe966c20d40fc172565bc28aaa2990c0a006360eed91af + languageName: node + linkType: hard + +"d3-scale@npm:4": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" + dependencies: + d3-array: "npm:2.10.0 - 3" + d3-format: "npm:1 - 3" + d3-interpolate: "npm:1.2.0 - 3" + d3-time: "npm:2.1.1 - 3" + d3-time-format: "npm:2 - 4" + checksum: 10c0/65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1 + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b + languageName: node + linkType: hard + +"d3-shape@npm:3": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: "npm:^3.1.0" + checksum: 10c0/f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132 + languageName: node + linkType: hard + +"d3-shape@npm:^1.2.0": + version: 1.3.7 + resolution: "d3-shape@npm:1.3.7" + dependencies: + d3-path: "npm:1" + checksum: 10c0/548057ce59959815decb449f15632b08e2a1bdce208f9a37b5f98ec7629dda986c2356bc7582308405ce68aedae7d47b324df41507404df42afaf352907577ae + languageName: node + linkType: hard + +"d3-time-format@npm:2 - 4, d3-time-format@npm:4": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" + dependencies: + d3-time: "npm:1 - 3" + checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206 + languageName: node + linkType: hard + +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: "npm:2 - 3" + checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3, d3-timer@npm:3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad + languageName: node + linkType: hard + +"d3-zoom@npm:3": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 + languageName: node + linkType: hard + +"d3@npm:^7.9.0": + version: 7.9.0 + resolution: "d3@npm:7.9.0" + dependencies: + d3-array: "npm:3" + d3-axis: "npm:3" + d3-brush: "npm:3" + d3-chord: "npm:3" + d3-color: "npm:3" + d3-contour: "npm:4" + d3-delaunay: "npm:6" + d3-dispatch: "npm:3" + d3-drag: "npm:3" + d3-dsv: "npm:3" + d3-ease: "npm:3" + d3-fetch: "npm:3" + d3-force: "npm:3" + d3-format: "npm:3" + d3-geo: "npm:3" + d3-hierarchy: "npm:3" + d3-interpolate: "npm:3" + d3-path: "npm:3" + d3-polygon: "npm:3" + d3-quadtree: "npm:3" + d3-random: "npm:3" + d3-scale: "npm:4" + d3-scale-chromatic: "npm:3" + d3-selection: "npm:3" + d3-shape: "npm:3" + d3-time: "npm:3" + d3-time-format: "npm:4" + d3-timer: "npm:3" + d3-transition: "npm:3" + d3-zoom: "npm:3" + checksum: 10c0/3dd9c08c73cfaa69c70c49e603c85e049c3904664d9c79a1a52a0f52795828a1ff23592dc9a7b2257e711d68a615472a13103c212032f38e016d609796e087e8 + languageName: node + linkType: hard + +"dagre-d3-es@npm:7.0.14": + version: 7.0.14 + resolution: "dagre-d3-es@npm:7.0.14" + dependencies: + d3: "npm:^7.9.0" + lodash-es: "npm:^4.17.21" + checksum: 10c0/0dc91fc79300eb0a4eab5a48a76c2baf3ce439c389d19e2f015729bb57dafd75e1e9a4c2880daf016e81ee45caca7b21745c13b23b6cd2a786ce84767e88323e + languageName: node + linkType: hard + "data-urls@npm:^5.0.0": version: 5.0.0 resolution: "data-urls@npm:5.0.0" @@ -9806,6 +10583,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.19": + version: 1.11.21 + resolution: "dayjs@npm:1.11.21" + checksum: 10c0/bd97dfdc4bfea3c66268635690313828b386faa040fbc1f829ff42a2bd748b72c9d9b3c8f9616ce9e61fcb78923f1461a462c969c54b1084458ae1b715898fb0 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -9966,6 +10750,15 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:5": + version: 5.1.0 + resolution: "delaunator@npm:5.1.0" + dependencies: + robust-predicates: "npm:^3.0.2" + checksum: 10c0/6489e3598212ab8759575e30f3ac26063471846e25c779048f441f2ccefd25efb9a52275e7e8e3dcc5bf5bc5c35169cf1de27f985d359fd5a24057be88fd1817 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -10471,6 +11264,20 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:^1.45.1": + version: 1.47.0 + resolution: "es-toolkit@npm:1.47.0" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + vitepress-plugin-sandpack@1.1.4: + unplugged: true + checksum: 10c0/6edc9709fccc409fa4adddd318971d8a18335de9225f2dc99021aacba44fe66a2437a831923589c643865caab261a087bd974338294a60bb5a74932caa102901 + languageName: node + linkType: hard + "esast-util-from-estree@npm:^2.0.0": version: 2.0.0 resolution: "esast-util-from-estree@npm:2.0.0" @@ -11635,6 +12442,13 @@ __metadata: languageName: node linkType: hard +"hachure-fill@npm:^0.5.2": + version: 0.5.2 + resolution: "hachure-fill@npm:0.5.2" + checksum: 10c0/307e3b6f9f2d3c11a82099c3f71eecbb9c440c00c1f896ac1732c23e6dbff16a92bb893d222b8b721b89cf11e58649ca60b4c24e5663f705f877cefd40153429 + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -12335,7 +13149,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -12435,6 +13249,13 @@ __metadata: languageName: node linkType: hard +"import-meta-resolve@npm:^4.2.0": + version: 4.2.0 + resolution: "import-meta-resolve@npm:4.2.0" + checksum: 10c0/3ee8aeecb61d19b49d2703987f977e9d1c7d4ba47db615a570eaa02fe414f40dfa63f7b953e842cbe8470d26df6371332bfcf21b2fd92b0112f9fea80dde2c4c + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -12544,6 +13365,20 @@ __metadata: languageName: node linkType: hard +"internmap@npm:1 - 2": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed + languageName: node + linkType: hard + +"internmap@npm:^1.0.0": + version: 1.0.1 + resolution: "internmap@npm:1.0.1" + checksum: 10c0/60942be815ca19da643b6d4f23bd0bf4e8c97abbd080fb963fe67583b60bdfb3530448ad4486bae40810e92317bded9995cc31411218acc750d72cd4e8646eee + languageName: node + linkType: hard + "invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -13865,6 +14700,17 @@ __metadata: languageName: node linkType: hard +"katex@npm:^0.16.25": + version: 0.16.47 + resolution: "katex@npm:0.16.47" + dependencies: + commander: "npm:^8.3.0" + bin: + katex: cli.js + checksum: 10c0/b10f4d0651c60771a48444879e4227255e26e2b2ec061b1ee4b08934863ad2324ba8dbb772455f7768aeb14dfcc13bcd309174a0ddd5ef954a607f644a197710 + languageName: node + linkType: hard + "keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -13874,6 +14720,13 @@ __metadata: languageName: node linkType: hard +"khroma@npm:^2.1.0": + version: 2.1.0 + resolution: "khroma@npm:2.1.0" + checksum: 10c0/634d98753ff5d2540491cafeb708fc98de0d43f4e6795256d5c8f6e3ad77de93049ea41433928fda3697adf7bbe6fe27351858f6d23b78f8b5775ef314c59891 + languageName: node + linkType: hard + "kind-of@npm:^6.0.0, kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -13916,6 +14769,20 @@ __metadata: languageName: node linkType: hard +"layout-base@npm:^1.0.0": + version: 1.0.2 + resolution: "layout-base@npm:1.0.2" + checksum: 10c0/2a55d0460fd9f6ed53d7e301b9eb3dea19bda03815d616a40665ce6dc75c1f4d62e1ca19a897da1cfaf6de1b91de59cd6f2f79ba1258f3d7fccc7d46ca7f3337 + languageName: node + linkType: hard + +"layout-base@npm:^2.0.0": + version: 2.0.1 + resolution: "layout-base@npm:2.0.1" + checksum: 10c0/a44df9ef3cbff9916a10f616635e22b5787c89fa62b2fec6f99e8e6ee512c7cebd22668ce32dab5a83c934ba0a309c51a678aa0b40d70853de6c357893c0a88b + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -14112,6 +14979,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.18.1 + resolution: "lodash-es@npm:4.18.1" + checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8 + languageName: node + linkType: hard + "lodash.debounce@npm:^4, lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -14305,6 +15179,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^16.3.0": + version: 16.4.2 + resolution: "marked@npm:16.4.2" + bin: + marked: bin/marked.js + checksum: 10c0/fc6051142172454f2023f3d6b31cca92879ec8e1b96457086a54c70354c74b00e1b6543a76a1fad6d399366f52b90a848f6ffb8e1d65a5baff87f3ba9b8f1847 + languageName: node + linkType: hard + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -14629,6 +15512,35 @@ __metadata: languageName: node linkType: hard +"mermaid@npm:>=11.6.0": + version: 11.15.0 + resolution: "mermaid@npm:11.15.0" + dependencies: + "@braintree/sanitize-url": "npm:^7.1.1" + "@iconify/utils": "npm:^3.0.2" + "@mermaid-js/parser": "npm:^1.1.1" + "@types/d3": "npm:^7.4.3" + "@upsetjs/venn.js": "npm:^2.0.0" + cytoscape: "npm:^3.33.1" + cytoscape-cose-bilkent: "npm:^4.1.0" + cytoscape-fcose: "npm:^2.2.0" + d3: "npm:^7.9.0" + d3-sankey: "npm:^0.12.3" + dagre-d3-es: "npm:7.0.14" + dayjs: "npm:^1.11.19" + dompurify: "npm:^3.3.1" + es-toolkit: "npm:^1.45.1" + katex: "npm:^0.16.25" + khroma: "npm:^2.1.0" + marked: "npm:^16.3.0" + roughjs: "npm:^4.6.6" + stylis: "npm:^4.3.6" + ts-dedent: "npm:^2.2.0" + uuid: "npm:^11.1.0 || ^12 || ^13 || ^14.0.0" + checksum: 10c0/9085b47c30b66a1e94e07c93951d9e53e5c89ec8c9fff8814233bddeecf6b8eaf9036d3168e4d1360c3f5d07cd051c666da96860eb0f22f064bd749499a0c83f + languageName: node + linkType: hard + "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -16093,6 +17005,13 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^1.3.0": + version: 1.6.0 + resolution: "package-manager-detector@npm:1.6.0" + checksum: 10c0/6419d0b840be64fd45bcdcb7a19f09b81b65456d5e7f7a3daac305a4c90643052122f6ac0308afe548ffee75e36148532a2002ea9d292754f1e385aa2e1ea03b + languageName: node + linkType: hard + "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -16243,6 +17162,13 @@ __metadata: languageName: node linkType: hard +"path-data-parser@npm:0.1.0, path-data-parser@npm:^0.1.0": + version: 0.1.0 + resolution: "path-data-parser@npm:0.1.0" + checksum: 10c0/ba22d54669a8bc4a3df27431fe667900685585d1196085b803d0aa4066b83e709bbf2be7c1d2b56e706b49cc698231d55947c22abbfc4843ca424bbf8c985745 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -16425,6 +17351,23 @@ __metadata: languageName: node linkType: hard +"points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0": + version: 0.2.0 + resolution: "points-on-curve@npm:0.2.0" + checksum: 10c0/f0d92343fcc2ad1f48334633e580574c1e0e28038a756133e171e537f270d6d64203feada5ee556e36f448a1b46e0306dee07b30f589f4e3ad720f6ee38ef48c + languageName: node + linkType: hard + +"points-on-path@npm:^0.2.1": + version: 0.2.1 + resolution: "points-on-path@npm:0.2.1" + dependencies: + path-data-parser: "npm:0.1.0" + points-on-curve: "npm:0.2.0" + checksum: 10c0/a7010340f9f196976f61838e767bb7b0b7f6273ab4fb9eb37c61001fe26fbfc3fcd63c96d5e85b9a4ab579213ab366f2ddaaf60e2a9253e2b91a62db33f395ba + languageName: node + linkType: hard + "popmotion@npm:9.3.6": version: 9.3.6 resolution: "popmotion@npm:9.3.6" @@ -18551,6 +19494,25 @@ __metadata: languageName: node linkType: hard +"robust-predicates@npm:^3.0.2": + version: 3.0.3 + resolution: "robust-predicates@npm:3.0.3" + checksum: 10c0/ae23a0318be809545f091a076818747848f144495491e24e6df5d767f2bad0a1501bbeac34e78b74681d1437ecff0585f593ebfe6c8d49a50e79c5053b962693 + languageName: node + linkType: hard + +"roughjs@npm:^4.6.6": + version: 4.6.6 + resolution: "roughjs@npm:4.6.6" + dependencies: + hachure-fill: "npm:^0.5.2" + path-data-parser: "npm:^0.1.0" + points-on-curve: "npm:^0.2.0" + points-on-path: "npm:^0.2.1" + checksum: 10c0/68c11bf4516aa014cef2fe52426a9bab237c2f500d13e1a4f13b523cb5723667bf2d92b9619325efdc5bc2a193588ff5af8d51683df17cfb8720e96fe2b92b0c + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.8.0": version: 0.8.0 resolution: "rrweb-cssom@npm:0.8.0" @@ -18602,6 +19564,13 @@ __metadata: languageName: node linkType: hard +"rw@npm:1": + version: 1.3.3 + resolution: "rw@npm:1.3.3" + checksum: 10c0/b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53 + languageName: node + linkType: hard + "rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": version: 7.8.2 resolution: "rxjs@npm:7.8.2" @@ -19600,6 +20569,13 @@ __metadata: languageName: node linkType: hard +"stylis@npm:^4.3.6": + version: 4.4.0 + resolution: "stylis@npm:4.4.0" + checksum: 10c0/259be096d90dfbfe903c8656dcb7591e52a421e577e950ef42ebd9ca02f387623a1165dd08761492fb6e92a7a562d62a53a694a10b0a2f6dcd7a0db107b4bf55 + languageName: node + linkType: hard + "supports-color@npm:^2.0.0": version: 2.0.0 resolution: "supports-color@npm:2.0.0" @@ -19893,6 +20869,7 @@ __metadata: "@docusaurus/plugin-content-docs": "npm:^3.10.1" "@docusaurus/preset-classic": "npm:^3.10.1" "@docusaurus/theme-common": "npm:^3.10.1" + "@docusaurus/theme-mermaid": "npm:^3.10.1" "@docusaurus/theme-search-algolia": "npm:^3.10.1" "@docusaurus/tsconfig": "npm:^3.10.1" "@docusaurus/types": "npm:^3.10.1" @@ -19971,6 +20948,13 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^1.0.1": + version: 1.2.3 + resolution: "tinyexec@npm:1.2.3" + checksum: 10c0/e4694310797ae576547f55a449bc4be6480ce1cd64ff22ca9768506172bb37475c3d79efa7257e93c88d96374933a8ba2f7567b8f0c7269380008884dec42a9a + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -20178,6 +21162,13 @@ __metadata: languageName: node linkType: hard +"ts-dedent@npm:^2.2.0": + version: 2.2.0 + resolution: "ts-dedent@npm:2.2.0" + checksum: 10c0/175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 + languageName: node + linkType: hard + "ts-jest@npm:^29.4.0": version: 29.4.0 resolution: "ts-jest@npm:29.4.0" @@ -20780,6 +21771,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.1.0 || ^12 || ^13 || ^14.0.0": + version: 14.0.0 + resolution: "uuid@npm:14.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 10c0/a57ae7794c45005c1a9208989196c5baf79a7679c30f43c1bee9033a2c4d113a2cea216fa6fcc9663b08b0d55635df1a7c6eb7e7f3d21c3e50688c698fa39a50 + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" From dcbfd002a30917e93e7ff075e96ee845d9500d1e Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 14:08:15 +0100 Subject: [PATCH 05/16] docs: use an SVG architecture diagram for Gateway WAF Replace the overview's ASCII pipeline with a static SVG (static/img/calico-enterprise/gateway-waf-architecture.svg) and revert the Mermaid introduction. @docusaurus/theme-mermaid is incompatible with this repo's multi-docs-instance + yarn-berry setup: @docusaurus/theme-common (peer dep @docusaurus/plugin-content-docs: "*") is instantiated as two virtual copies, so the classic theme's and theme-mermaid's useColorMode() bind different React contexts -> ReactContextError during both worker-thread SSG and client render. The SVG carries descriptive alt text for accessibility. --- .../threat/gateway-waf/index.mdx | 27 +- docusaurus.config.js | 1 - package.json | 1 - .../gateway-waf-architecture.svg | 64 ++ yarn.lock | 1016 +---------------- 5 files changed, 76 insertions(+), 1033 deletions(-) create mode 100644 static/img/calico-enterprise/gateway-waf-architecture.svg diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx index b4d79fa80b..5965e0c175 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -4,8 +4,6 @@ hide_table_of_contents: true --- import { DocCardLink, DocCardLinkLayout } from '/src/___new___/components'; -import BrowserOnly from '@docusaurus/BrowserOnly'; -import Mermaid from '@theme/Mermaid'; # Gateway WAF @@ -29,27 +27,10 @@ Gateway WAF runs the industry-standard [OWASP ModSecurity Core Rule Set v4](http Gateway WAF is configured entirely through Kubernetes resources. The controller merges every policy and plugin that applies to a target, generates an ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy` that loads the Coraza WASM filter. - - {() => ( - directive allowlist +
per-namespace SecRule ID range"] - end - subgraph rt["Reconcile time"] - M["Merge policies + plugins"] --> DIR["Ordered SecLang directives"] - DIR --> VAL{"Rego validation"} - end - subgraph dp["Data plane"] - EEP["EnvoyExtensionPolicy
Coraza WASM filter"] --> EG["Envoy Gateway"] --> REQ["Request inspected"] - end - W -->|apply accepted| M - VAL -->|pass| EEP - VAL -->|"Enforce + Critical: fail"| REJ["No filter programmed
status reports the violation"] -`} - /> - )} -
+Gateway WAF request flow. GlobalWAFPolicy/WAFPolicy and GlobalWAFPlugin/WAFPlugin are merged into an ordered list of SecLang directives. Rego validation then either passes the result to an EnvoyExtensionPolicy that loads the Coraza WASM filter on Envoy Gateway, or, on an Enforce-mode Critical violation, programs no filter and reports the violation on status. The admission webhook validates submitted rules at apply time. The inputs to the merge are `GlobalWAFPolicy` + `WAFPolicy` and `GlobalWAFPlugin` + `WAFPlugin`. Two guardrails apply: the admission webhook (structural, at write time) and validation policies (compliance, after the merge). diff --git a/docusaurus.config.js b/docusaurus.config.js index 919772cc2e..1bbdc541bb 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -45,7 +45,6 @@ export default async function createAsyncConfig() { onBrokenMarkdownLinks: 'throw', }, }, - themes: ['@docusaurus/theme-mermaid'], // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want diff --git a/package.json b/package.json index f01298b769..9bf70d216b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@docusaurus/plugin-content-docs": "^3.10.1", "@docusaurus/preset-classic": "^3.10.1", "@docusaurus/theme-common": "^3.10.1", - "@docusaurus/theme-mermaid": "^3.10.1", "@docusaurus/theme-search-algolia": "^3.10.1", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", diff --git a/static/img/calico-enterprise/gateway-waf-architecture.svg b/static/img/calico-enterprise/gateway-waf-architecture.svg new file mode 100644 index 0000000000..a432ad2150 --- /dev/null +++ b/static/img/calico-enterprise/gateway-waf-architecture.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + Admission webhook + directive allowlist + per-namespace SecRule ID range + + + + GlobalWAFPolicy + WAFPolicy + GlobalWAFPlugin + WAFPlugin + + + + Merge + + + + Ordered SecLang + directives + + + + Rego + validation + + + + EnvoyExtensionPolicy + Coraza WASM filter + + + + Envoy Gateway → request + + + + No filter programmed + status reports the violation + + + + + + + pass + + + + fail (Enforce + Critical) + + + at apply + diff --git a/yarn.lock b/yarn.lock index fd980d83fd..a1954dc61c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -430,16 +430,6 @@ __metadata: languageName: node linkType: hard -"@antfu/install-pkg@npm:^1.1.0": - version: 1.1.0 - resolution: "@antfu/install-pkg@npm:1.1.0" - dependencies: - package-manager-detector: "npm:^1.3.0" - tinyexec: "npm:^1.0.1" - checksum: 10c0/140d5994c76fd3d0e824c88f1ce91b3370e8066a8bc2f5729ae133bf768caa239f7915e29c78f239b7ead253113ace51293e95127fafe2b786b88eb615b3be47 - languageName: node - linkType: hard - "@apify/consts@npm:^2.20.0, @apify/consts@npm:^2.43.0": version: 2.43.0 resolution: "@apify/consts@npm:2.43.0" @@ -2066,13 +2056,6 @@ __metadata: languageName: node linkType: hard -"@braintree/sanitize-url@npm:^7.1.1": - version: 7.1.2 - resolution: "@braintree/sanitize-url@npm:7.1.2" - checksum: 10c0/62f2aa0cf58626e3880b2dc1025c42064b4639abd157ae4e1c35f4c2f5031e9273772046a423979845069c814e27ff818e8e669280dc53585e6f033d5b7a59cb - languageName: node - linkType: hard - "@chakra-ui/anatomy@npm:2.2.2": version: 2.2.2 resolution: "@chakra-ui/anatomy@npm:2.2.2" @@ -2377,13 +2360,6 @@ __metadata: languageName: node linkType: hard -"@chevrotain/types@npm:~11.1.1": - version: 11.1.2 - resolution: "@chevrotain/types@npm:11.1.2" - checksum: 10c0/c0c4679a3d407df34e18d5adfa7ac599b4a2bfddbf68da6e43678b9b3e16ab911de7766b37b9fc466261c3dead3db1b620e2e344f800fa9f0f381720475eda8f - languageName: node - linkType: hard - "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -3787,28 +3763,6 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-mermaid@npm:^3.10.1": - version: 3.10.1 - resolution: "@docusaurus/theme-mermaid@npm:3.10.1" - dependencies: - "@docusaurus/core": "npm:3.10.1" - "@docusaurus/module-type-aliases": "npm:3.10.1" - "@docusaurus/theme-common": "npm:3.10.1" - "@docusaurus/types": "npm:3.10.1" - "@docusaurus/utils-validation": "npm:3.10.1" - mermaid: "npm:>=11.6.0" - tslib: "npm:^2.6.0" - peerDependencies: - "@mermaid-js/layout-elk": ^0.1.9 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@mermaid-js/layout-elk": - optional: true - checksum: 10c0/73839669eb3e69ec565a16853b6a8fa636f3fd94d6656976835a0304bb6e7402b5dcbd7808b8b158bb0f34e61fee1d835511d88f59f48fa613eb51532614992c - languageName: node - linkType: hard - "@docusaurus/theme-search-algolia@npm:3.10.1, @docusaurus/theme-search-algolia@npm:^3.10.1": version: 3.10.1 resolution: "@docusaurus/theme-search-algolia@npm:3.10.1" @@ -4164,24 +4118,6 @@ __metadata: languageName: node linkType: hard -"@iconify/types@npm:^2.0.0": - version: 2.0.0 - resolution: "@iconify/types@npm:2.0.0" - checksum: 10c0/65a3be43500c7ccacf360e136d00e1717f050b7b91da644e94370256ac66f582d59212bdb30d00788aab4fc078262e91c95b805d1808d654b72f6d2072a7e4b2 - languageName: node - linkType: hard - -"@iconify/utils@npm:^3.0.2": - version: 3.1.3 - resolution: "@iconify/utils@npm:3.1.3" - dependencies: - "@antfu/install-pkg": "npm:^1.1.0" - "@iconify/types": "npm:^2.0.0" - import-meta-resolve: "npm:^4.2.0" - checksum: 10c0/6d01196172ef062a9cd0ee299dff3a405221e65057d6cca9ac9fb4bacd7d78168d54b0837b75d5f163038d770a3a7e4449d68ea2fc7a577c01b9ab51bfd53c98 - languageName: node - linkType: hard - "@inquirer/figures@npm:^1.0.3": version: 1.0.12 resolution: "@inquirer/figures@npm:1.0.12" @@ -4794,15 +4730,6 @@ __metadata: languageName: node linkType: hard -"@mermaid-js/parser@npm:^1.1.1": - version: 1.1.1 - resolution: "@mermaid-js/parser@npm:1.1.1" - dependencies: - "@chevrotain/types": "npm:~11.1.1" - checksum: 10c0/66414d9d66f3a86a6751427cb8890a00beaaee2b7eba34c21b34b60cdaf9237d21e9b50b9bf1f560a01a600ee332d33b949fd8f12cbda3238aba50edc0dfdf15 - languageName: node - linkType: hard - "@module-federation/error-codes@npm:0.22.0": version: 0.22.0 resolution: "@module-federation/error-codes@npm:0.22.0" @@ -6833,278 +6760,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:*": - version: 3.2.2 - resolution: "@types/d3-array@npm:3.2.2" - checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028 - languageName: node - linkType: hard - -"@types/d3-axis@npm:*": - version: 3.0.6 - resolution: "@types/d3-axis@npm:3.0.6" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a - languageName: node - linkType: hard - -"@types/d3-brush@npm:*": - version: 3.0.6 - resolution: "@types/d3-brush@npm:3.0.6" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 - languageName: node - linkType: hard - -"@types/d3-chord@npm:*": - version: 3.0.6 - resolution: "@types/d3-chord@npm:3.0.6" - checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 - languageName: node - linkType: hard - -"@types/d3-color@npm:*": - version: 3.1.3 - resolution: "@types/d3-color@npm:3.1.3" - checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae - languageName: node - linkType: hard - -"@types/d3-contour@npm:*": - version: 3.0.6 - resolution: "@types/d3-contour@npm:3.0.6" - dependencies: - "@types/d3-array": "npm:*" - "@types/geojson": "npm:*" - checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c - languageName: node - linkType: hard - -"@types/d3-delaunay@npm:*": - version: 6.0.4 - resolution: "@types/d3-delaunay@npm:6.0.4" - checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 - languageName: node - linkType: hard - -"@types/d3-dispatch@npm:*": - version: 3.0.7 - resolution: "@types/d3-dispatch@npm:3.0.7" - checksum: 10c0/38c6605ebf0bf0099dfb70eafe0dd4ae8213368b40b8f930b72a909ff2e7259d2bd8a54d100bb5a44eb4b36f4f2a62dcb37f8be59613ca6b507c7a2f910b3145 - languageName: node - linkType: hard - -"@types/d3-drag@npm:*": - version: 3.0.7 - resolution: "@types/d3-drag@npm:3.0.7" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d - languageName: node - linkType: hard - -"@types/d3-dsv@npm:*": - version: 3.0.7 - resolution: "@types/d3-dsv@npm:3.0.7" - checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 - languageName: node - linkType: hard - -"@types/d3-ease@npm:*": - version: 3.0.2 - resolution: "@types/d3-ease@npm:3.0.2" - checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c - languageName: node - linkType: hard - -"@types/d3-fetch@npm:*": - version: 3.0.7 - resolution: "@types/d3-fetch@npm:3.0.7" - dependencies: - "@types/d3-dsv": "npm:*" - checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 - languageName: node - linkType: hard - -"@types/d3-force@npm:*": - version: 3.0.10 - resolution: "@types/d3-force@npm:3.0.10" - checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92 - languageName: node - linkType: hard - -"@types/d3-format@npm:*": - version: 3.0.4 - resolution: "@types/d3-format@npm:3.0.4" - checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee - languageName: node - linkType: hard - -"@types/d3-geo@npm:*": - version: 3.1.0 - resolution: "@types/d3-geo@npm:3.1.0" - dependencies: - "@types/geojson": "npm:*" - checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 - languageName: node - linkType: hard - -"@types/d3-hierarchy@npm:*": - version: 3.1.7 - resolution: "@types/d3-hierarchy@npm:3.1.7" - checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 - languageName: node - linkType: hard - -"@types/d3-interpolate@npm:*": - version: 3.0.4 - resolution: "@types/d3-interpolate@npm:3.0.4" - dependencies: - "@types/d3-color": "npm:*" - checksum: 10c0/066ebb8da570b518dd332df6b12ae3b1eaa0a7f4f0c702e3c57f812cf529cc3500ec2aac8dc094f31897790346c6b1ebd8cd7a077176727f4860c2b181a65ca4 - languageName: node - linkType: hard - -"@types/d3-path@npm:*": - version: 3.1.1 - resolution: "@types/d3-path@npm:3.1.1" - checksum: 10c0/2c36eb31ebaf2ce4712e793fd88087117976f7c4ed69cc2431825f999c8c77cca5cea286f3326432b770739ac6ccd5d04d851eb65e7a4dbcc10c982b49ad2c02 - languageName: node - linkType: hard - -"@types/d3-polygon@npm:*": - version: 3.0.2 - resolution: "@types/d3-polygon@npm:3.0.2" - checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 - languageName: node - linkType: hard - -"@types/d3-quadtree@npm:*": - version: 3.0.6 - resolution: "@types/d3-quadtree@npm:3.0.6" - checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 - languageName: node - linkType: hard - -"@types/d3-random@npm:*": - version: 3.0.3 - resolution: "@types/d3-random@npm:3.0.3" - checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 - languageName: node - linkType: hard - -"@types/d3-scale-chromatic@npm:*": - version: 3.1.0 - resolution: "@types/d3-scale-chromatic@npm:3.1.0" - checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2 - languageName: node - linkType: hard - -"@types/d3-scale@npm:*": - version: 4.0.9 - resolution: "@types/d3-scale@npm:4.0.9" - dependencies: - "@types/d3-time": "npm:*" - checksum: 10c0/4ac44233c05cd50b65b33ecb35d99fdf07566bcdbc55bc1306b2f27d1c5134d8c560d356f2c8e76b096e9125ffb8d26d95f78d56e210d1c542cb255bdf31d6c8 - languageName: node - linkType: hard - -"@types/d3-selection@npm:*": - version: 3.0.11 - resolution: "@types/d3-selection@npm:3.0.11" - checksum: 10c0/0c512956c7503ff5def4bb32e0c568cc757b9a2cc400a104fc0f4cfe5e56d83ebde2a97821b6f2cb26a7148079d3b86a2f28e11d68324ed311cf35c2ed980d1d - languageName: node - linkType: hard - -"@types/d3-shape@npm:*": - version: 3.1.8 - resolution: "@types/d3-shape@npm:3.1.8" - dependencies: - "@types/d3-path": "npm:*" - checksum: 10c0/49ec2172b9eb66fc1d036e2a23966216bb972e9af51ddbed134df24bd71aedf207bb1ef81903a119eb4e1f5e927cf44beacaf64c9af86474e5548594b102b574 - languageName: node - linkType: hard - -"@types/d3-time-format@npm:*": - version: 4.0.3 - resolution: "@types/d3-time-format@npm:4.0.3" - checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 - languageName: node - linkType: hard - -"@types/d3-time@npm:*": - version: 3.0.4 - resolution: "@types/d3-time@npm:3.0.4" - checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174 - languageName: node - linkType: hard - -"@types/d3-timer@npm:*": - version: 3.0.2 - resolution: "@types/d3-timer@npm:3.0.2" - checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 - languageName: node - linkType: hard - -"@types/d3-transition@npm:*": - version: 3.0.9 - resolution: "@types/d3-transition@npm:3.0.9" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/4f68b9df7ac745b3491216c54203cbbfa0f117ae4c60e2609cdef2db963582152035407fdff995b10ee383bae2f05b7743493f48e1b8e46df54faa836a8fb7b5 - languageName: node - linkType: hard - -"@types/d3-zoom@npm:*": - version: 3.0.8 - resolution: "@types/d3-zoom@npm:3.0.8" - dependencies: - "@types/d3-interpolate": "npm:*" - "@types/d3-selection": "npm:*" - checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547 - languageName: node - linkType: hard - -"@types/d3@npm:^7.4.3": - version: 7.4.3 - resolution: "@types/d3@npm:7.4.3" - dependencies: - "@types/d3-array": "npm:*" - "@types/d3-axis": "npm:*" - "@types/d3-brush": "npm:*" - "@types/d3-chord": "npm:*" - "@types/d3-color": "npm:*" - "@types/d3-contour": "npm:*" - "@types/d3-delaunay": "npm:*" - "@types/d3-dispatch": "npm:*" - "@types/d3-drag": "npm:*" - "@types/d3-dsv": "npm:*" - "@types/d3-ease": "npm:*" - "@types/d3-fetch": "npm:*" - "@types/d3-force": "npm:*" - "@types/d3-format": "npm:*" - "@types/d3-geo": "npm:*" - "@types/d3-hierarchy": "npm:*" - "@types/d3-interpolate": "npm:*" - "@types/d3-path": "npm:*" - "@types/d3-polygon": "npm:*" - "@types/d3-quadtree": "npm:*" - "@types/d3-random": "npm:*" - "@types/d3-scale": "npm:*" - "@types/d3-scale-chromatic": "npm:*" - "@types/d3-selection": "npm:*" - "@types/d3-shape": "npm:*" - "@types/d3-time": "npm:*" - "@types/d3-time-format": "npm:*" - "@types/d3-timer": "npm:*" - "@types/d3-transition": "npm:*" - "@types/d3-zoom": "npm:*" - checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 - languageName: node - linkType: hard - "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -7209,13 +6864,6 @@ __metadata: languageName: node linkType: hard -"@types/geojson@npm:*": - version: 7946.0.16 - resolution: "@types/geojson@npm:7946.0.16" - checksum: 10c0/1ff24a288bd5860b766b073ead337d31d73bdc715e5b50a2cee5cb0af57a1ed02cc04ef295f5fa68dc40fe3e4f104dd31282b2b818a5ba3231bc1001ba084e3c - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -7837,21 +7485,6 @@ __metadata: languageName: node linkType: hard -"@upsetjs/venn.js@npm:^2.0.0": - version: 2.0.0 - resolution: "@upsetjs/venn.js@npm:2.0.0" - dependencies: - d3-selection: "npm:^3.0.0" - d3-transition: "npm:^3.0.1" - dependenciesMeta: - d3-selection: - optional: true - d3-transition: - optional: true - checksum: 10c0/b12014d94708ab4df7f5a4b6205c6f23ff235cca2ffe91df3314862b109b826e52f9020c2a2f7527d3712d21c578d6db9cdb60ce46a528739cc18e58d111f724 - languageName: node - linkType: hard - "@vladfrangu/async_event_emitter@npm:^2.2.2": version: 2.4.6 resolution: "@vladfrangu/async_event_emitter@npm:2.4.6" @@ -9547,13 +9180,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:7, commander@npm:^7.2.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a - languageName: node - linkType: hard - "commander@npm:^10.0.0": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -9575,6 +9201,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -9770,24 +9403,6 @@ __metadata: languageName: node linkType: hard -"cose-base@npm:^1.0.0": - version: 1.0.3 - resolution: "cose-base@npm:1.0.3" - dependencies: - layout-base: "npm:^1.0.0" - checksum: 10c0/a6e400b1d101393d6af0967c1353355777c1106c40417c5acaef6ca8bdda41e2fc9398f466d6c85be30290943ad631f2590569f67b3fd5368a0d8318946bd24f - languageName: node - linkType: hard - -"cose-base@npm:^2.2.0": - version: 2.2.0 - resolution: "cose-base@npm:2.2.0" - dependencies: - layout-base: "npm:^2.0.0" - checksum: 10c0/14b9f8100ac322a00777ffb1daeb3321af368bbc9cabe3103943361273baee2003202ffe38e4ab770960b600214224e9c196195a78d589521540aa694df7cdec - languageName: node - linkType: hard - "cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -10181,398 +9796,6 @@ __metadata: languageName: node linkType: hard -"cytoscape-cose-bilkent@npm:^4.1.0": - version: 4.1.0 - resolution: "cytoscape-cose-bilkent@npm:4.1.0" - dependencies: - cose-base: "npm:^1.0.0" - peerDependencies: - cytoscape: ^3.2.0 - checksum: 10c0/5e2480ddba9da1a68e700ed2c674cbfd51e9efdbd55788f1971a68de4eb30708e3b3a5e808bf5628f7a258680406bbe6586d87a9133e02a9bdc1ab1a92f512f2 - languageName: node - linkType: hard - -"cytoscape-fcose@npm:^2.2.0": - version: 2.2.0 - resolution: "cytoscape-fcose@npm:2.2.0" - dependencies: - cose-base: "npm:^2.2.0" - peerDependencies: - cytoscape: ^3.2.0 - checksum: 10c0/ce472c9f85b9057e75c5685396f8e1f2468895e71b184913e05ad56dcf3092618fe59a1054f29cb0995051ba8ebe566ad0dd49a58d62845145624bd60cd44917 - languageName: node - linkType: hard - -"cytoscape@npm:^3.33.1": - version: 3.33.4 - resolution: "cytoscape@npm:3.33.4" - checksum: 10c0/a5bd5dfd7c0f0a0e646407a4b0671fdcf45014a637d72f041bc80f51befca49477559f3ad5e4a8cf362196d079c22ae2f890632ecf17ad64f2934ce59848ebe7 - languageName: node - linkType: hard - -"d3-array@npm:1 - 2": - version: 2.12.1 - resolution: "d3-array@npm:2.12.1" - dependencies: - internmap: "npm:^1.0.0" - checksum: 10c0/7eca10427a9f113a4ca6a0f7301127cab26043fd5e362631ef5a0edd1c4b2dd70c56ed317566700c31e4a6d88b55f3951aaba192291817f243b730cb2352882e - languageName: node - linkType: hard - -"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": - version: 3.2.4 - resolution: "d3-array@npm:3.2.4" - dependencies: - internmap: "npm:1 - 2" - checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50 - languageName: node - linkType: hard - -"d3-axis@npm:3": - version: 3.0.0 - resolution: "d3-axis@npm:3.0.0" - checksum: 10c0/a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78 - languageName: node - linkType: hard - -"d3-brush@npm:3": - version: 3.0.0 - resolution: "d3-brush@npm:3.0.0" - dependencies: - d3-dispatch: "npm:1 - 3" - d3-drag: "npm:2 - 3" - d3-interpolate: "npm:1 - 3" - d3-selection: "npm:3" - d3-transition: "npm:3" - checksum: 10c0/07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8 - languageName: node - linkType: hard - -"d3-chord@npm:3": - version: 3.0.1 - resolution: "d3-chord@npm:3.0.1" - dependencies: - d3-path: "npm:1 - 3" - checksum: 10c0/baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce - languageName: node - linkType: hard - -"d3-color@npm:1 - 3, d3-color@npm:3": - version: 3.1.0 - resolution: "d3-color@npm:3.1.0" - checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c - languageName: node - linkType: hard - -"d3-contour@npm:4": - version: 4.0.2 - resolution: "d3-contour@npm:4.0.2" - dependencies: - d3-array: "npm:^3.2.0" - checksum: 10c0/98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032 - languageName: node - linkType: hard - -"d3-delaunay@npm:6": - version: 6.0.4 - resolution: "d3-delaunay@npm:6.0.4" - dependencies: - delaunator: "npm:5" - checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754 - languageName: node - linkType: hard - -"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3": - version: 3.0.1 - resolution: "d3-dispatch@npm:3.0.1" - checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 - languageName: node - linkType: hard - -"d3-drag@npm:2 - 3, d3-drag@npm:3": - version: 3.0.0 - resolution: "d3-drag@npm:3.0.0" - dependencies: - d3-dispatch: "npm:1 - 3" - d3-selection: "npm:3" - checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa - languageName: node - linkType: hard - -"d3-dsv@npm:1 - 3, d3-dsv@npm:3": - version: 3.0.1 - resolution: "d3-dsv@npm:3.0.1" - dependencies: - commander: "npm:7" - iconv-lite: "npm:0.6" - rw: "npm:1" - bin: - csv2json: bin/dsv2json.js - csv2tsv: bin/dsv2dsv.js - dsv2dsv: bin/dsv2dsv.js - dsv2json: bin/dsv2json.js - json2csv: bin/json2dsv.js - json2dsv: bin/json2dsv.js - json2tsv: bin/json2dsv.js - tsv2csv: bin/dsv2dsv.js - tsv2json: bin/dsv2json.js - checksum: 10c0/10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab - languageName: node - linkType: hard - -"d3-ease@npm:1 - 3, d3-ease@npm:3": - version: 3.0.1 - resolution: "d3-ease@npm:3.0.1" - checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 - languageName: node - linkType: hard - -"d3-fetch@npm:3": - version: 3.0.1 - resolution: "d3-fetch@npm:3.0.1" - dependencies: - d3-dsv: "npm:1 - 3" - checksum: 10c0/4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b - languageName: node - linkType: hard - -"d3-force@npm:3": - version: 3.0.0 - resolution: "d3-force@npm:3.0.0" - dependencies: - d3-dispatch: "npm:1 - 3" - d3-quadtree: "npm:1 - 3" - d3-timer: "npm:1 - 3" - checksum: 10c0/220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf - languageName: node - linkType: hard - -"d3-format@npm:1 - 3, d3-format@npm:3": - version: 3.1.2 - resolution: "d3-format@npm:3.1.2" - checksum: 10c0/0de452ae07585238e7f01607a7e0066665c34609652188b6ac7dc9f424f69465a425e07d16d79bd0e5955202ac7f241c66d0c76f68a79fc6f4857c94cf420652 - languageName: node - linkType: hard - -"d3-geo@npm:3": - version: 3.1.1 - resolution: "d3-geo@npm:3.1.1" - dependencies: - d3-array: "npm:2.5.0 - 3" - checksum: 10c0/d32270dd2dc8ac3ea63e8805d63239c4c8ec6c0d339d73b5e5a30a87f8f54db22a78fb434369799465eae169503b25f9a107c642c8a16c32a3285bc0e6d8e8c1 - languageName: node - linkType: hard - -"d3-hierarchy@npm:3": - version: 3.1.2 - resolution: "d3-hierarchy@npm:3.1.2" - checksum: 10c0/6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf - languageName: node - linkType: hard - -"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3": - version: 3.0.1 - resolution: "d3-interpolate@npm:3.0.1" - dependencies: - d3-color: "npm:1 - 3" - checksum: 10c0/19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a - languageName: node - linkType: hard - -"d3-path@npm:1": - version: 1.0.9 - resolution: "d3-path@npm:1.0.9" - checksum: 10c0/e35e84df5abc18091f585725b8235e1fa97efc287571585427d3a3597301e6c506dea56b11dfb3c06ca5858b3eb7f02c1bf4f6a716aa9eade01c41b92d497eb5 - languageName: node - linkType: hard - -"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": - version: 3.1.0 - resolution: "d3-path@npm:3.1.0" - checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da - languageName: node - linkType: hard - -"d3-polygon@npm:3": - version: 3.0.1 - resolution: "d3-polygon@npm:3.0.1" - checksum: 10c0/e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df - languageName: node - linkType: hard - -"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": - version: 3.0.1 - resolution: "d3-quadtree@npm:3.0.1" - checksum: 10c0/18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090 - languageName: node - linkType: hard - -"d3-random@npm:3": - version: 3.0.1 - resolution: "d3-random@npm:3.0.1" - checksum: 10c0/987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41 - languageName: node - linkType: hard - -"d3-sankey@npm:^0.12.3": - version: 0.12.3 - resolution: "d3-sankey@npm:0.12.3" - dependencies: - d3-array: "npm:1 - 2" - d3-shape: "npm:^1.2.0" - checksum: 10c0/261debb01a13269f6fc53b9ebaef174a015d5ad646242c23995bf514498829ab8b8f920a7873724a7494288b46bea3ce7ebc5a920b745bc8ae4caa5885cf5204 - languageName: node - linkType: hard - -"d3-scale-chromatic@npm:3": - version: 3.1.0 - resolution: "d3-scale-chromatic@npm:3.1.0" - dependencies: - d3-color: "npm:1 - 3" - d3-interpolate: "npm:1 - 3" - checksum: 10c0/9a3f4671ab0b971f4a411b42180d7cf92bfe8e8584e637ce7e698d705e18d6d38efbd20ec64f60cc0dfe966c20d40fc172565bc28aaa2990c0a006360eed91af - languageName: node - linkType: hard - -"d3-scale@npm:4": - version: 4.0.2 - resolution: "d3-scale@npm:4.0.2" - dependencies: - d3-array: "npm:2.10.0 - 3" - d3-format: "npm:1 - 3" - d3-interpolate: "npm:1.2.0 - 3" - d3-time: "npm:2.1.1 - 3" - d3-time-format: "npm:2 - 4" - checksum: 10c0/65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1 - languageName: node - linkType: hard - -"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": - version: 3.0.0 - resolution: "d3-selection@npm:3.0.0" - checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b - languageName: node - linkType: hard - -"d3-shape@npm:3": - version: 3.2.0 - resolution: "d3-shape@npm:3.2.0" - dependencies: - d3-path: "npm:^3.1.0" - checksum: 10c0/f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132 - languageName: node - linkType: hard - -"d3-shape@npm:^1.2.0": - version: 1.3.7 - resolution: "d3-shape@npm:1.3.7" - dependencies: - d3-path: "npm:1" - checksum: 10c0/548057ce59959815decb449f15632b08e2a1bdce208f9a37b5f98ec7629dda986c2356bc7582308405ce68aedae7d47b324df41507404df42afaf352907577ae - languageName: node - linkType: hard - -"d3-time-format@npm:2 - 4, d3-time-format@npm:4": - version: 4.1.0 - resolution: "d3-time-format@npm:4.1.0" - dependencies: - d3-time: "npm:1 - 3" - checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206 - languageName: node - linkType: hard - -"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": - version: 3.1.0 - resolution: "d3-time@npm:3.1.0" - dependencies: - d3-array: "npm:2 - 3" - checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1 - languageName: node - linkType: hard - -"d3-timer@npm:1 - 3, d3-timer@npm:3": - version: 3.0.1 - resolution: "d3-timer@npm:3.0.1" - checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a - languageName: node - linkType: hard - -"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1": - version: 3.0.1 - resolution: "d3-transition@npm:3.0.1" - dependencies: - d3-color: "npm:1 - 3" - d3-dispatch: "npm:1 - 3" - d3-ease: "npm:1 - 3" - d3-interpolate: "npm:1 - 3" - d3-timer: "npm:1 - 3" - peerDependencies: - d3-selection: 2 - 3 - checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad - languageName: node - linkType: hard - -"d3-zoom@npm:3": - version: 3.0.0 - resolution: "d3-zoom@npm:3.0.0" - dependencies: - d3-dispatch: "npm:1 - 3" - d3-drag: "npm:2 - 3" - d3-interpolate: "npm:1 - 3" - d3-selection: "npm:2 - 3" - d3-transition: "npm:2 - 3" - checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 - languageName: node - linkType: hard - -"d3@npm:^7.9.0": - version: 7.9.0 - resolution: "d3@npm:7.9.0" - dependencies: - d3-array: "npm:3" - d3-axis: "npm:3" - d3-brush: "npm:3" - d3-chord: "npm:3" - d3-color: "npm:3" - d3-contour: "npm:4" - d3-delaunay: "npm:6" - d3-dispatch: "npm:3" - d3-drag: "npm:3" - d3-dsv: "npm:3" - d3-ease: "npm:3" - d3-fetch: "npm:3" - d3-force: "npm:3" - d3-format: "npm:3" - d3-geo: "npm:3" - d3-hierarchy: "npm:3" - d3-interpolate: "npm:3" - d3-path: "npm:3" - d3-polygon: "npm:3" - d3-quadtree: "npm:3" - d3-random: "npm:3" - d3-scale: "npm:4" - d3-scale-chromatic: "npm:3" - d3-selection: "npm:3" - d3-shape: "npm:3" - d3-time: "npm:3" - d3-time-format: "npm:4" - d3-timer: "npm:3" - d3-transition: "npm:3" - d3-zoom: "npm:3" - checksum: 10c0/3dd9c08c73cfaa69c70c49e603c85e049c3904664d9c79a1a52a0f52795828a1ff23592dc9a7b2257e711d68a615472a13103c212032f38e016d609796e087e8 - languageName: node - linkType: hard - -"dagre-d3-es@npm:7.0.14": - version: 7.0.14 - resolution: "dagre-d3-es@npm:7.0.14" - dependencies: - d3: "npm:^7.9.0" - lodash-es: "npm:^4.17.21" - checksum: 10c0/0dc91fc79300eb0a4eab5a48a76c2baf3ce439c389d19e2f015729bb57dafd75e1e9a4c2880daf016e81ee45caca7b21745c13b23b6cd2a786ce84767e88323e - languageName: node - linkType: hard - "data-urls@npm:^5.0.0": version: 5.0.0 resolution: "data-urls@npm:5.0.0" @@ -10583,13 +9806,6 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.19": - version: 1.11.21 - resolution: "dayjs@npm:1.11.21" - checksum: 10c0/bd97dfdc4bfea3c66268635690313828b386faa040fbc1f829ff42a2bd748b72c9d9b3c8f9616ce9e61fcb78923f1461a462c969c54b1084458ae1b715898fb0 - languageName: node - linkType: hard - "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -10750,15 +9966,6 @@ __metadata: languageName: node linkType: hard -"delaunator@npm:5": - version: 5.1.0 - resolution: "delaunator@npm:5.1.0" - dependencies: - robust-predicates: "npm:^3.0.2" - checksum: 10c0/6489e3598212ab8759575e30f3ac26063471846e25c779048f441f2ccefd25efb9a52275e7e8e3dcc5bf5bc5c35169cf1de27f985d359fd5a24057be88fd1817 - languageName: node - linkType: hard - "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -11264,20 +10471,6 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.45.1": - version: 1.47.0 - resolution: "es-toolkit@npm:1.47.0" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - vitepress-plugin-sandpack@1.1.4: - unplugged: true - checksum: 10c0/6edc9709fccc409fa4adddd318971d8a18335de9225f2dc99021aacba44fe66a2437a831923589c643865caab261a087bd974338294a60bb5a74932caa102901 - languageName: node - linkType: hard - "esast-util-from-estree@npm:^2.0.0": version: 2.0.0 resolution: "esast-util-from-estree@npm:2.0.0" @@ -12442,13 +11635,6 @@ __metadata: languageName: node linkType: hard -"hachure-fill@npm:^0.5.2": - version: 0.5.2 - resolution: "hachure-fill@npm:0.5.2" - checksum: 10c0/307e3b6f9f2d3c11a82099c3f71eecbb9c440c00c1f896ac1732c23e6dbff16a92bb893d222b8b721b89cf11e58649ca60b4c24e5663f705f877cefd40153429 - languageName: node - linkType: hard - "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -13149,7 +12335,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -13249,13 +12435,6 @@ __metadata: languageName: node linkType: hard -"import-meta-resolve@npm:^4.2.0": - version: 4.2.0 - resolution: "import-meta-resolve@npm:4.2.0" - checksum: 10c0/3ee8aeecb61d19b49d2703987f977e9d1c7d4ba47db615a570eaa02fe414f40dfa63f7b953e842cbe8470d26df6371332bfcf21b2fd92b0112f9fea80dde2c4c - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -13365,20 +12544,6 @@ __metadata: languageName: node linkType: hard -"internmap@npm:1 - 2": - version: 2.0.3 - resolution: "internmap@npm:2.0.3" - checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed - languageName: node - linkType: hard - -"internmap@npm:^1.0.0": - version: 1.0.1 - resolution: "internmap@npm:1.0.1" - checksum: 10c0/60942be815ca19da643b6d4f23bd0bf4e8c97abbd080fb963fe67583b60bdfb3530448ad4486bae40810e92317bded9995cc31411218acc750d72cd4e8646eee - languageName: node - linkType: hard - "invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -14700,17 +13865,6 @@ __metadata: languageName: node linkType: hard -"katex@npm:^0.16.25": - version: 0.16.47 - resolution: "katex@npm:0.16.47" - dependencies: - commander: "npm:^8.3.0" - bin: - katex: cli.js - checksum: 10c0/b10f4d0651c60771a48444879e4227255e26e2b2ec061b1ee4b08934863ad2324ba8dbb772455f7768aeb14dfcc13bcd309174a0ddd5ef954a607f644a197710 - languageName: node - linkType: hard - "keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -14720,13 +13874,6 @@ __metadata: languageName: node linkType: hard -"khroma@npm:^2.1.0": - version: 2.1.0 - resolution: "khroma@npm:2.1.0" - checksum: 10c0/634d98753ff5d2540491cafeb708fc98de0d43f4e6795256d5c8f6e3ad77de93049ea41433928fda3697adf7bbe6fe27351858f6d23b78f8b5775ef314c59891 - languageName: node - linkType: hard - "kind-of@npm:^6.0.0, kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -14769,20 +13916,6 @@ __metadata: languageName: node linkType: hard -"layout-base@npm:^1.0.0": - version: 1.0.2 - resolution: "layout-base@npm:1.0.2" - checksum: 10c0/2a55d0460fd9f6ed53d7e301b9eb3dea19bda03815d616a40665ce6dc75c1f4d62e1ca19a897da1cfaf6de1b91de59cd6f2f79ba1258f3d7fccc7d46ca7f3337 - languageName: node - linkType: hard - -"layout-base@npm:^2.0.0": - version: 2.0.1 - resolution: "layout-base@npm:2.0.1" - checksum: 10c0/a44df9ef3cbff9916a10f616635e22b5787c89fa62b2fec6f99e8e6ee512c7cebd22668ce32dab5a83c934ba0a309c51a678aa0b40d70853de6c357893c0a88b - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -14979,13 +14112,6 @@ __metadata: languageName: node linkType: hard -"lodash-es@npm:^4.17.21": - version: 4.18.1 - resolution: "lodash-es@npm:4.18.1" - checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8 - languageName: node - linkType: hard - "lodash.debounce@npm:^4, lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -15179,15 +14305,6 @@ __metadata: languageName: node linkType: hard -"marked@npm:^16.3.0": - version: 16.4.2 - resolution: "marked@npm:16.4.2" - bin: - marked: bin/marked.js - checksum: 10c0/fc6051142172454f2023f3d6b31cca92879ec8e1b96457086a54c70354c74b00e1b6543a76a1fad6d399366f52b90a848f6ffb8e1d65a5baff87f3ba9b8f1847 - languageName: node - linkType: hard - "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -15512,35 +14629,6 @@ __metadata: languageName: node linkType: hard -"mermaid@npm:>=11.6.0": - version: 11.15.0 - resolution: "mermaid@npm:11.15.0" - dependencies: - "@braintree/sanitize-url": "npm:^7.1.1" - "@iconify/utils": "npm:^3.0.2" - "@mermaid-js/parser": "npm:^1.1.1" - "@types/d3": "npm:^7.4.3" - "@upsetjs/venn.js": "npm:^2.0.0" - cytoscape: "npm:^3.33.1" - cytoscape-cose-bilkent: "npm:^4.1.0" - cytoscape-fcose: "npm:^2.2.0" - d3: "npm:^7.9.0" - d3-sankey: "npm:^0.12.3" - dagre-d3-es: "npm:7.0.14" - dayjs: "npm:^1.11.19" - dompurify: "npm:^3.3.1" - es-toolkit: "npm:^1.45.1" - katex: "npm:^0.16.25" - khroma: "npm:^2.1.0" - marked: "npm:^16.3.0" - roughjs: "npm:^4.6.6" - stylis: "npm:^4.3.6" - ts-dedent: "npm:^2.2.0" - uuid: "npm:^11.1.0 || ^12 || ^13 || ^14.0.0" - checksum: 10c0/9085b47c30b66a1e94e07c93951d9e53e5c89ec8c9fff8814233bddeecf6b8eaf9036d3168e4d1360c3f5d07cd051c666da96860eb0f22f064bd749499a0c83f - languageName: node - linkType: hard - "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -17005,13 +16093,6 @@ __metadata: languageName: node linkType: hard -"package-manager-detector@npm:^1.3.0": - version: 1.6.0 - resolution: "package-manager-detector@npm:1.6.0" - checksum: 10c0/6419d0b840be64fd45bcdcb7a19f09b81b65456d5e7f7a3daac305a4c90643052122f6ac0308afe548ffee75e36148532a2002ea9d292754f1e385aa2e1ea03b - languageName: node - linkType: hard - "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -17162,13 +16243,6 @@ __metadata: languageName: node linkType: hard -"path-data-parser@npm:0.1.0, path-data-parser@npm:^0.1.0": - version: 0.1.0 - resolution: "path-data-parser@npm:0.1.0" - checksum: 10c0/ba22d54669a8bc4a3df27431fe667900685585d1196085b803d0aa4066b83e709bbf2be7c1d2b56e706b49cc698231d55947c22abbfc4843ca424bbf8c985745 - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -17351,23 +16425,6 @@ __metadata: languageName: node linkType: hard -"points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0": - version: 0.2.0 - resolution: "points-on-curve@npm:0.2.0" - checksum: 10c0/f0d92343fcc2ad1f48334633e580574c1e0e28038a756133e171e537f270d6d64203feada5ee556e36f448a1b46e0306dee07b30f589f4e3ad720f6ee38ef48c - languageName: node - linkType: hard - -"points-on-path@npm:^0.2.1": - version: 0.2.1 - resolution: "points-on-path@npm:0.2.1" - dependencies: - path-data-parser: "npm:0.1.0" - points-on-curve: "npm:0.2.0" - checksum: 10c0/a7010340f9f196976f61838e767bb7b0b7f6273ab4fb9eb37c61001fe26fbfc3fcd63c96d5e85b9a4ab579213ab366f2ddaaf60e2a9253e2b91a62db33f395ba - languageName: node - linkType: hard - "popmotion@npm:9.3.6": version: 9.3.6 resolution: "popmotion@npm:9.3.6" @@ -19494,25 +18551,6 @@ __metadata: languageName: node linkType: hard -"robust-predicates@npm:^3.0.2": - version: 3.0.3 - resolution: "robust-predicates@npm:3.0.3" - checksum: 10c0/ae23a0318be809545f091a076818747848f144495491e24e6df5d767f2bad0a1501bbeac34e78b74681d1437ecff0585f593ebfe6c8d49a50e79c5053b962693 - languageName: node - linkType: hard - -"roughjs@npm:^4.6.6": - version: 4.6.6 - resolution: "roughjs@npm:4.6.6" - dependencies: - hachure-fill: "npm:^0.5.2" - path-data-parser: "npm:^0.1.0" - points-on-curve: "npm:^0.2.0" - points-on-path: "npm:^0.2.1" - checksum: 10c0/68c11bf4516aa014cef2fe52426a9bab237c2f500d13e1a4f13b523cb5723667bf2d92b9619325efdc5bc2a193588ff5af8d51683df17cfb8720e96fe2b92b0c - languageName: node - linkType: hard - "rrweb-cssom@npm:^0.8.0": version: 0.8.0 resolution: "rrweb-cssom@npm:0.8.0" @@ -19564,13 +18602,6 @@ __metadata: languageName: node linkType: hard -"rw@npm:1": - version: 1.3.3 - resolution: "rw@npm:1.3.3" - checksum: 10c0/b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53 - languageName: node - linkType: hard - "rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": version: 7.8.2 resolution: "rxjs@npm:7.8.2" @@ -20569,13 +19600,6 @@ __metadata: languageName: node linkType: hard -"stylis@npm:^4.3.6": - version: 4.4.0 - resolution: "stylis@npm:4.4.0" - checksum: 10c0/259be096d90dfbfe903c8656dcb7591e52a421e577e950ef42ebd9ca02f387623a1165dd08761492fb6e92a7a562d62a53a694a10b0a2f6dcd7a0db107b4bf55 - languageName: node - linkType: hard - "supports-color@npm:^2.0.0": version: 2.0.0 resolution: "supports-color@npm:2.0.0" @@ -20869,7 +19893,6 @@ __metadata: "@docusaurus/plugin-content-docs": "npm:^3.10.1" "@docusaurus/preset-classic": "npm:^3.10.1" "@docusaurus/theme-common": "npm:^3.10.1" - "@docusaurus/theme-mermaid": "npm:^3.10.1" "@docusaurus/theme-search-algolia": "npm:^3.10.1" "@docusaurus/tsconfig": "npm:^3.10.1" "@docusaurus/types": "npm:^3.10.1" @@ -20948,13 +19971,6 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.1": - version: 1.2.3 - resolution: "tinyexec@npm:1.2.3" - checksum: 10c0/e4694310797ae576547f55a449bc4be6480ce1cd64ff22ca9768506172bb37475c3d79efa7257e93c88d96374933a8ba2f7567b8f0c7269380008884dec42a9a - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.12": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -21162,13 +20178,6 @@ __metadata: languageName: node linkType: hard -"ts-dedent@npm:^2.2.0": - version: 2.2.0 - resolution: "ts-dedent@npm:2.2.0" - checksum: 10c0/175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 - languageName: node - linkType: hard - "ts-jest@npm:^29.4.0": version: 29.4.0 resolution: "ts-jest@npm:29.4.0" @@ -21771,15 +20780,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.1.0 || ^12 || ^13 || ^14.0.0": - version: 14.0.0 - resolution: "uuid@npm:14.0.0" - bin: - uuid: dist-node/bin/uuid - checksum: 10c0/a57ae7794c45005c1a9208989196c5baf79a7679c30f43c1bee9033a2c4d113a2cea216fa6fcc9663b08b0d55635df1a7c6eb7e7f3d21c3e50688c698fa39a50 - languageName: node - linkType: hard - "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" From 96bb1139686f9f158ae56d4cc9e5ebd5ac085c27 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Fri, 29 May 2026 23:07:17 +0100 Subject: [PATCH 06/16] docs: tune Gateway WAF architecture SVG layout Fix text overflow and tighten spacing flagged in review: widen the canvas to 940x360, split the admission-webhook caption to two lines, widen the input/process boxes so every label fits with margin, and clean up the pass/fail arrow routing. Same colour palette. --- .../gateway-waf-architecture.svg | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/static/img/calico-enterprise/gateway-waf-architecture.svg b/static/img/calico-enterprise/gateway-waf-architecture.svg index a432ad2150..135ebd9eac 100644 --- a/static/img/calico-enterprise/gateway-waf-architecture.svg +++ b/static/img/calico-enterprise/gateway-waf-architecture.svg @@ -1,64 +1,62 @@ - + - - - - - - + + + - + - - Admission webhook - directive allowlist + per-namespace SecRule ID range + + Admission webhook + directive allowlist + + per-namespace SecRule ID range - - GlobalWAFPolicy + WAFPolicy - GlobalWAFPlugin + WAFPlugin + + GlobalWAFPolicy + WAFPolicy + GlobalWAFPlugin + WAFPlugin - - Merge + + Merge - - Ordered SecLang - directives + + Ordered SecLang + directives - - Rego - validation + + Rego + validation - - EnvoyExtensionPolicy - Coraza WASM filter + + EnvoyExtensionPolicy + Coraza WASM filter - - Envoy Gateway → request + + Envoy Gateway → request - - No filter programmed - status reports the violation + + No filter programmed + status reports the violation - - - - - pass - + + + + + pass + + + + at apply - - fail (Enforce + Critical) - - - at apply + + fail (Enforce + Critical) From 4bd1a91ec8b5c0f165c4066edcd22f204e68ef00 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Mon, 6 Jul 2026 16:56:38 +0100 Subject: [PATCH 07/16] docs(gateway-waf): clarify SecRule ID range is an allowance, not a unique reservation (EV-6757) The range page implied each namespace gets a private, collision-free block. Reword to say the range is the set of IDs your rules may use, note it isn't globally unique (two namespaces can map to the same block, harmlessly), and that tenant isolation comes from per-namespace engines plus the protected ranges. --- .../threat/gateway-waf/reference/secrule-id-ranges.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx index 6b8824997b..ace3c0af28 100644 --- a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx +++ b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx @@ -4,7 +4,7 @@ description: How Gateway WAF assigns each namespace a SecRule ID range, which ru # SecRule ID ranges -Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. To keep one namespace from colliding with the IDs of another — or disabling the cluster's baseline rules — Gateway WAF assigns each namespace its own range of IDs and enforces it through an admission webhook. +Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. Gateway WAF gives each namespace a defined range of IDs to use, and stops any namespace from disabling the cluster's baseline rules. Both are enforced through an admission webhook. ## How a namespace's range is derived @@ -15,7 +15,7 @@ prefix = first four hex digits of sha256(namespace name), modulo 10000 range = [ prefix * 10000 , prefix * 10000 + 9999 ] ``` -Because the range is a pure function of the namespace name, it never changes and requires no cluster state. Across the ID space this gives up to 10,000 namespaces a private block of 10,000 IDs each. +Because the range is a pure function of the namespace name, it never changes and requires no cluster state. This range is the set of IDs your rules may use. It isn't a globally unique reservation: with 10,000 possible blocks, two namespaces can map to the same one, which is harmless. Tenants stay isolated because each namespace's rules run in their own WAF engine, and no namespace can touch the protected ranges below. ## Finding your range From a63dcd0143c6e576d5809ccf5117e6f6f8530b7f Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 14:59:42 +0100 Subject: [PATCH 08/16] docs(gateway-waf): consolidate 16 pages into 6 Fold the concepts/, get-started/, how-to/, and reference/ subpages into single Overview, How it works, Get started, Common tasks, Reference, and Troubleshooting pages, flattening the section to match the threat/ layout. Content preserved; sidebar updated. Ref: EV-6466 --- .../threat/gateway-waf/common-tasks.mdx | 579 ++++++++++++++++++ .../concepts/global-vs-namespaced.mdx | 61 -- .../concepts/merge-and-enforcement.mdx | 75 --- .../concepts/policies-plugins-validation.mdx | 53 -- .../quickstart.mdx => get-started.mdx} | 116 +++- .../threat/gateway-waf/get-started/enable.mdx | 99 --- .../threat/gateway-waf/how-it-works.mdx | 159 +++++ .../how-to/attach-waf-to-a-route.mdx | 227 ------- .../gateway-waf/how-to/multi-tenant-setup.mdx | 284 --------- .../gateway-waf/how-to/set-a-baseline.mdx | 174 ------ .../gateway-waf/how-to/validate-config.mdx | 257 -------- .../gateway-waf/how-to/write-custom-rules.mdx | 195 ------ .../threat/gateway-waf/index.mdx | 15 +- .../threat/gateway-waf/reference.mdx | 369 +++++++++++ .../threat/gateway-waf/reference/crds.mdx | 210 ------- .../gateway-waf/reference/rego-validation.mdx | 81 --- .../reference/secrule-id-ranges.mdx | 70 --- .../reference/status-conditions.mdx | 82 --- .../threat/gateway-waf/troubleshooting.mdx | 26 +- sidebars-calico-enterprise.js | 42 +- 20 files changed, 1209 insertions(+), 1965 deletions(-) create mode 100644 calico-enterprise/threat/gateway-waf/common-tasks.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx rename calico-enterprise/threat/gateway-waf/{get-started/quickstart.mdx => get-started.mdx} (64%) delete mode 100644 calico-enterprise/threat/gateway-waf/get-started/enable.mdx create mode 100644 calico-enterprise/threat/gateway-waf/how-it-works.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx create mode 100644 calico-enterprise/threat/gateway-waf/reference.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/reference/crds.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx diff --git a/calico-enterprise/threat/gateway-waf/common-tasks.mdx b/calico-enterprise/threat/gateway-waf/common-tasks.mdx new file mode 100644 index 0000000000..38132fe71b --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/common-tasks.mdx @@ -0,0 +1,579 @@ +--- +description: Set a cluster-wide WAF baseline, attach WAF to a Gateway or route, write custom rules, validate configuration with Rego, and run a full multi-tenant setup. +--- + +# Common tasks + +## Set a cluster-wide baseline + +:::note[Cluster operator task] + +This task is for cluster operators who own the `GlobalWAFPolicy` singleton and set the security posture that all namespaces inherit. + +::: + +Before any application operator can attach WAF to a route, you need a cluster baseline. The baseline defines the default action, Core Rule Set state, paranoia level, and any organization-wide custom rules. Application operators inherit these settings; they can tighten them, but a [validation policy](#validate-configuration-with-rego) can prevent them from weakening them. + +**Prerequisites:** the Gateway WAF feature gate is enabled on the `ApplicationLayer` resource (see [Get started](./get-started.mdx)), and you have cluster-admin rights — the `GlobalWAFPolicy` API is cluster-scoped. + +### Create the GlobalWAFPolicy singleton + +`GlobalWAFPolicy` must be named `default`. Applying any other name is rejected by the admission webhook. + +Start in `Detect` mode. `Detect` logs matched requests but allows them through, giving you time to measure false positives before you switch to `Block`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Detect # start here; promote to Block after validating false-positive rate + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +``` + +Apply it with `kubectl apply -f globalwafpolicy.yaml`. + +The OWASP Core Rule Set defines four paranoia levels. Each higher level adds more detection rules and catches more attacks, at the cost of more false positives: + +| Level | Posture | Typical use | +| :--- | :--- | :--- | +| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | +| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | +| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | +| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | + +:::tip + +Start at paranoia level 1 cluster-wide. Let application operators raise the level for their own namespaces if their traffic demands it. You can enforce a minimum level later with a validation policy. + +::: + +Once you've confirmed the false-positive rate is acceptable, change `defaultAction` to `Block`. In `Block` mode, $[prodname] returns HTTP 403 for any request that matches a WAF rule. + +### Add organization-wide rules with a GlobalWAFPlugin + +A `GlobalWAFPlugin` holds custom ModSecurity rules that apply to every namespace through the global policy's `plugins[]` list. Create the plugin first, then reference it from `GlobalWAFPolicy`. Rules are organized into the four lifecycle slots described in [Write custom rules](#write-custom-rules-with-plugins). + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: org-baseline +spec: + description: "Organization-wide security baseline rules" + config: | + SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | + SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ + "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" + SecRule TX:ENFORCE_HTTPS "@eq 1" "" + rules: | + SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ + "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" + after: | + # Cluster operator's after slot runs last in the merge order. + SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ + "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" +``` + +Reference it from `GlobalWAFPolicy.spec.plugins[]`: + +```yaml +spec: + plugins: + - kind: GlobalWAFPlugin + name: org-baseline +``` + +`plugins[]` is an ordered list; the merger processes plugins in array position order. Add more `GlobalWAFPlugin` entries as your organization grows — up to 64 total. You can add `WAFPlugin` entries to `GlobalWAFPolicy.spec.plugins[]` as well. + +### Verify the baseline + +```bash +kubectl get globalwafpolicy default +``` + +``` +NAME ACCEPTED AGE +default True 30s +``` + +For detailed status, inspect `.status.conditions` with `kubectl get globalwafpolicy default -o yaml`. An `Accepted=True` condition with reason `Reconciled` means the baseline is programmed. If a validation policy blocked the configuration, you will see `Accepted=False` with a `ValidationFailed` reason and a message describing the violation. See [Status conditions](./reference.mdx#status-conditions) for the full condition reference. + +Every namespace that has a `WAFPolicy` attached to a target inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy`. A `WAFPolicy` can override any of these fields for its namespace; unset fields fall back to the global value: + +``` +GlobalWAFPolicy (baseline) + └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 + └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy +``` + +--- + +## Attach WAF to a Gateway or route + +:::note[Application operator task] + +This task is for application operators who own namespace-scoped WAF resources and decide which of their Gateways and routes receive WAF inspection. + +::: + +WAF inspection does not start automatically when the cluster operator enables the feature. You activate it by creating a `WAFPolicy` in your namespace that points to one or more Gateway API targets. The policy inherits the cluster baseline and lets you layer namespace-specific overrides on top. + +**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy` (see above), you have write access to `WAFPolicy` resources in your namespace, and a `Gateway` or `HTTPRoute` in your namespace is already receiving traffic. + +`WAFPolicy.spec.targetRefs[]` accepts two kinds of targets: + +| `kind` in `targetRefs` | WAF covers | Use when | +| :--- | :--- | :--- | +| `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | +| `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | + +Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` on Envoy Gateway. A policy with two entries generates two filters — one per target. Up to 16 entries are allowed in `targetRefs`. + +:::note + +`sectionName` (targeting a specific listener on a Gateway) is reserved for a future release. Omit it. + +::: + +This example attaches WAF to a single `HTTPRoute` and overrides the action and paranoia level for this namespace. Unset fields (here, `coreRuleSet.state`) still inherit from the cluster baseline. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-api-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override: Block even if global baseline is Detect + coreRuleSet: + paranoiaLevel: 2 # stricter than the cluster default + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +Apply it with `kubectl apply -f wafpolicy.yaml`. + +To add rules beyond the cluster baseline, create a `WAFPlugin` and reference it from `WAFPolicy.spec.plugins[]` — see [Write custom rules](#write-custom-rules-with-plugins). + +For how `action` and `coreRuleSet` fields inherit from `GlobalWAFPolicy`, see [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart). + +### Conflict resolution + +Only one `WAFPolicy` may be the active policy for any given target at a time. If two `WAFPolicy` resources in the same namespace both target the same Gateway or HTTPRoute, $[prodname] follows the [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) tie-breaking rule: the policy with the oldest `creationTimestamp` wins, and the newer policy is marked `Accepted=False` with reason `Conflicted`. Only the winning policy's directives are merged and programmed. + +To resolve a conflict, delete the newer policy or update its `targetRefs` to point at a different target. + +### Verify the policy + +```bash +kubectl get wafpolicy payments-api-waf -n payments +``` + +``` +NAME ACCEPTED PROGRAMMED AGE +payments-api-waf True True 45s +``` + +The `.status.conditions` field reports `Accepted`, `Programmed`, and any validation findings — see [Status conditions](./reference.mdx#status-conditions). To confirm Envoy Gateway received the filter, run `kubectl get envoyextensionpolicies -n payments`. Each `targetRefs` entry produces one `EnvoyExtensionPolicy`. + +### Test the WAF + +Send a request that includes a classic SQL injection pattern. In `Block` mode this should return HTTP 403: + +```bash +curl -si "https:///api?id=1+OR+1%3D1" +``` + +``` +HTTP/1.1 403 Forbidden +content-length: 11 +content-type: text/plain + +Access denied +``` + +:::tip + +If the Gateway is running in `Detect` mode (the cluster default until you upgrade to `Block`), the request will be allowed through but the WAF event will appear in the Envoy Gateway logs. Check with: + +```bash +kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "waf\|coraza\|modsec" +``` + +::: + +:::caution + +If you get `ACCEPTED=False` with reason `ValidationFailed`, a `GlobalWAFValidationPolicy` is blocking your configuration. The status message identifies which policy and rule triggered the violation. Contact your cluster operator or see [Validate configuration with Rego](#validate-configuration-with-rego) for the self-check workflow. + +::: + +--- + +## Write custom rules with plugins + +:::note[Application operator task] + +This task is for application operators who manage WAF configuration within a namespace. Cluster operators who want organization-wide rules create a `GlobalWAFPlugin` instead and reference it from `GlobalWAFPolicy.spec.plugins[]` — the CRD fields are identical; only scope differs. + +::: + +A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots, modeled on the Core Rule Set 4 plugin file structure. Understanding the load order (see [How it works](./how-it-works.mdx#the-merged-ruleset-load-order)) is the key to knowing which slot to use: + +- **`config`** — variable setup. Runs before Core Rule Set setup. Use it to set `tx.*` transaction variables the Core Rule Set reads during its own initialization: tuning anomaly score thresholds, enabling optional Core Rule Set features, or setting application-specific flags. If a rule fires on a request variable (such as a request header), it belongs in `before` instead — at `config` time the request has not been read yet. +- **`before`** — pre-Core Rule Set rules. Runs after variable setup but before the Core Rule Set loads. Use it for early-exit allowlisting of trusted paths, request normalization, or setting flags that later rules can read. +- **`rules`** — custom rules alongside Core Rule Set. Runs after the full Core Rule Set has executed. This is a Tigera extension to the Core Rule Set plugin model. Use it for application-specific detections that complement the Core Rule Set rather than pre-empting it; because these run after the Core Rule Set, they can also read and act on anomaly scores. +- **`after`** — post-Core Rule Set processing. Runs last. Use it for false-positive suppression, response inspection, or logging augmentation. Because `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, the cluster operator has the final say on any post-processing. + +:::caution + +`ctl:ruleEngine=Off` inside a chained rule or as a per-request action disables the engine for that transaction only. Disabling it globally via a top-level `SecRuleEngine Off` directive is not permitted and will be rejected by the admission webhook. + +::: + +:::note + +`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected Core Rule Set baseline rules regardless of slot — the admission webhook will reject the attempt. + +::: + +### Rule IDs and your namespace's range + +Every `SecRule` and `SecAction` must have an ID. Gateway WAF assigns each namespace a contiguous block of 10,000 IDs and enforces it at admission time. Find your range before writing any rule: + +```bash +kubectl get namespace \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +# example output: 420000-429999 +``` + +Pick all your rule IDs from inside this range. The annotation is populated the first time any plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the full directive allowlist. + +### Worked example + +The following `WAFPlugin` uses all four slots to protect a GraphQL API: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: graphql-waf + namespace: platform +spec: + description: "GraphQL introspection guard and false-positive exceptions" + + config: | + # Flag the application type so later rules can branch on it. + SecAction \ + "id:420001,phase:1,pass,nolog,\ + setvar:tx.is_graphql=1" + + before: | + # Allowlist the schema endpoint used by the internal schema registry — + # skip WAF analysis entirely for that specific path. + SecRule REQUEST_URI "@streq /graphql/schema-registry" \ + "id:420010,phase:1,pass,nolog,\ + ctl:ruleEngine=Off,\ + tag:allowlist" + + rules: | + # Block introspection queries (production guard). + SecRule TX:IS_GRAPHQL "@eq 1" \ + "id:420020,phase:2,deny,status:403,\ + msg:'GraphQL introspection blocked',\ + logdata:'body_snippet=%{MATCHED_VAR}',\ + chain" + SecRule REQUEST_BODY "@rx __schema|__type" "" + + # Block suspicious user-agents known to scan GraphQL APIs. + SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ + "id:420021,phase:1,deny,status:403,\ + msg:'Known GraphQL scanner blocked',\ + logdata:'ua=%{MATCHED_VAR}'" + + after: | + # Remove a false positive: CRS rule 941130 fires on a legitimate + # Content-Type value used by our GraphQL client library. + SecRuleRemoveById 941130 +``` + +:::tip + +The `@pmf` operator loads a data file. Data files must be bundled into the Coraza WASM filter image. Ask your cluster operator whether a custom data file is available, or use `@pm` (phrase match inline) for a small list of strings instead. + +::: + +A plugin has no effect until a `WAFPolicy` references it. Add it to `spec.plugins[]`; load order is array position: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: platform-api-waf + namespace: platform +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: graphql-api + action: Block + plugins: + - kind: WAFPlugin + name: graphql-waf # ← your plugin above +``` + +If your policy also references a `GlobalWAFPlugin`, the global plugin's slots load before the namespace plugin's slots at each position in the merge order. + +Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The Core Rule Set documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. + +:::note + +Not all SecLang directives are permitted. Directives that read from the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. + +::: + +After applying, check that the plugin was accepted and that the generated `WAFPolicy` reconciled without errors: + +```bash +kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" +kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" +``` + +A `Ready=True` condition on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed in Envoy Gateway. + +--- + +## Validate configuration with Rego + +Gateway WAF has two guardrail layers. The admission webhook checks raw SecLang at apply time (structural safety). Validation policies check the **merged result** at reconcile time — after all policies and plugins have been combined — so they can catch problems that are only visible in the assembled directive list. For the full enforcement matrix, see [How it works](./how-it-works.mdx#validation-and-enforcement); for the input schema and authoring contract, see [Rego validation](./reference.mdx#rego-validation). + +### Cluster operator: GlobalWAFValidationPolicy + +:::note[Cluster operator] + +`GlobalWAFValidationPolicy` is a cluster-scoped resource. Only cluster operators can create or modify it. It applies to every namespace, or to a label-selected subset via `namespaceSelector`. + +::: + +A `GlobalWAFValidationPolicy` carries a list of Rego rules and an `enforcementMode`: `Audit` (default) records violations on status while the WAF filter is still programmed; `Enforce` blocks the filter from being programmed on a `Critical` violation. `Warning` and `Info` violations are recorded on status regardless of mode. + +The following policy enforces three requirements for all namespaces labeled `env: production`, and audits a fourth: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: production-requirements +spec: + enforcementMode: Enforce + namespaceSelector: + matchLabels: + env: production + rules: + + # Critical: CRS must be enabled + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } + + # Critical: production namespaces must use Block mode + - name: block-mode-required + severity: Critical + rego: | + package waf + violations[{"msg": "WAF must be in Block mode for production"}] { + input.config.action != "Block" + } + + # Critical: minimum paranoia level + - name: min-paranoia-level + severity: Critical + rego: | + package waf + violations[{"msg": msg}] { + input.config.paranoiaLevel < 2 + msg := sprintf( + "paranoia level %d is below the production minimum of 2", + [input.config.paranoiaLevel] + ) + } + + # Warning: advise against disabling RCE rules (does not block) + - name: rce-rules-intact + severity: Warning + rego: | + package waf + violations[{"msg": msg}] { + d := input.directives[_] + startswith(d, "SecRuleRemoveById") + re_match(`93[0-9]{4}`, d) + msg := sprintf("RCE rule removal detected in merged config: %s", [d]) + } +``` + +:::tip + +Start every new `GlobalWAFValidationPolicy` in `Audit` mode. Review the violations that appear on existing `WAFPolicy` status before switching to `Enforce`. This avoids blocking teams whose configurations predate the new policy. + +::: + +Leave `namespaceSelector` empty (or omit it) to apply the policy to every namespace, or scope it to a subset: + +```yaml +spec: + namespaceSelector: + matchLabels: + compliance: pci-dss # only namespaces with this label +``` + +Multiple `GlobalWAFValidationPolicy` objects can coexist. Every matching policy's rules are evaluated; a single `Critical` violation from any of them (when in `Enforce` mode) is enough to block programming. + +### Application operator: WAFValidationPolicy + +:::note[Application operator] + +`WAFValidationPolicy` is namespace-scoped. It is advisory only — violations appear on status but never block the WAF filter from being programmed. Use it as a self-check for your namespace's configuration. + +::: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFValidationPolicy +metadata: + name: team-self-check + namespace: platform +spec: + rules: + + - name: graphql-plugin-active + severity: Warning + rego: | + package waf + violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { + not "graphql-waf" in input.source.namespacePlugins + } +``` + +Because `WAFValidationPolicy` is advisory, it is safe to define your own standards here that are stricter than the cluster baseline — for example, requiring a minimum paranoia level higher than the global default. Violations surface on the `WAFPolicy` status and prompt the author to fix the config, but they do not interrupt traffic. + +### Reading violation results + +Violations from all matching validation policies appear in the status conditions of the affected `WAFPolicy`. Check them with `kubectl get wafpolicy -n -o yaml`. + +A blocked policy looks like: + +```yaml +status: + conditions: + - type: Ready + status: "False" + reason: ValidationFailed + message: > + GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": + WAF must be in Block mode for production + - type: Accepted + status: "True" +``` + +An audit violation (filter still programmed) looks like: + +```yaml +status: + conditions: + - type: Ready + status: "True" + - type: ValidationWarning + status: "True" + reason: AuditViolation + message: > + GlobalWAFValidationPolicy/production-requirements rule "rce-rules-intact" [Warning]: + RCE rule removal detected in merged config: SecRuleRemoveById 932100 +``` + +See [Status conditions](./reference.mdx#status-conditions) for the complete condition type reference. + +:::tip + +Write your Rego rules against the input schema in [Rego validation](./reference.mdx#rego-validation) and test them locally with `opa eval` before applying to the cluster. A syntax error in a rule causes the entire policy to be marked invalid; the status reports which rule failed to parse. + +::: + +--- + +## Multi-tenant setup + +:::note[Scenario] + +A single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. This walks through the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. + +**Prerequisites:** Gateway WAF is enabled and a valid license is in place. + +::: + +The cluster operator creates the `default` `GlobalWAFPolicy`, the `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` in `Enforce` mode scoped to namespaces labeled `env: production` — following the patterns above. Application operators need to read `Global*` resources to understand what baseline they are inheriting. Grant read access with a `ClusterRole` and a per-namespace `RoleBinding`: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: waf-global-reader +rules: + - apiGroups: ["applicationlayer.projectcalico.org"] + resources: + - globalwafpolicies + - globalwafplugins + - globalwafvalidationpolicies + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: waf-global-reader + namespace: team-a # repeat for payments +subjects: + - kind: Group + name: team-a-operators + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: waf-global-reader + apiGroup: rbac.authorization.k8s.io +``` + +:::note + +This grants read on cluster-scoped resources via a namespace-scoped `RoleBinding`. Application operators can see the baseline and validation policy, but cannot create or edit `Global*` resources. + +::: + +Each team labels its namespace (`kubectl label namespace team-a env=production`, and the same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. The `team-a` operator raises paranoia to 2 and inherits `coreRuleSet.state: Enabled` from the baseline. The `payments` operator runs stricter still: paranoia 3, plus a namespace `WAFPlugin` that blocks GraphQL introspection using an ID from its own allocated [SecRule ID range](./reference.mdx#secrule-id-ranges). + +### What happens if a team tries to weaken the baseline + +If the `payments` operator applies a policy with `action: Detect`, the `production-requirements` `GlobalWAFValidationPolicy` (in `Enforce` mode) fires the `must-use-block-mode` Critical rule. The controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation in `status.validation.failures`: + +```bash +kubectl get wafpolicy payments-waf -n payments \ + -o jsonpath='{.status.validation.failures}' | jq +``` + +No `EnvoyExtensionPolicy` is generated until the policy is corrected. The operator must restore `action: Block` or request a policy exception from the cluster operator. + +### Result: isolation in practice + +- **`team-a`** runs Core Rule Set at paranoia 2 with its own custom rules, scoped to its ID range. +- **`payments`** runs Core Rule Set at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. +- Neither team can disable the org-wide rule from `GlobalWAFPlugin` or lower below Block mode. +- The cluster operator sees all validation violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — a single place to audit which namespaces are out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx b/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx deleted file mode 100644 index 976a5e6cda..0000000000 --- a/calico-enterprise/threat/gateway-waf/concepts/global-vs-namespaced.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -description: How cluster-scoped and namespace-scoped Gateway WAF resources relate — ownership, the global singleton, and field inheritance. ---- - -# Global vs. namespaced - -Every Gateway WAF resource family has a cluster-scoped member and a namespace-scoped member. The split is deliberate: it maps the WAF configuration onto the two people who operate the cluster. - -| | Cluster-scoped | Namespace-scoped | -| :--- | :--- | :--- | -| Owner | Cluster operator (CO) | Application operator (AO) | -| Resources | `GlobalWAFPolicy`, `GlobalWAFPlugin`, `GlobalWAFValidationPolicy` | `WAFPolicy`, `WAFPlugin`, `WAFValidationPolicy` | -| Role | Set the baseline and the guardrails | Attach WAF to traffic and tune within the guardrails | - -The cluster operator *sets* the security posture; the application operator *tunes* it for their workloads. An AO can make their namespace stricter, and can be prevented from making it weaker through [validation policies](merge-and-enforcement.mdx#validation-and-enforcement). - -## The global policy is a singleton - -`GlobalWAFPolicy` is a cluster-wide singleton: it must be named `default`. It establishes the cluster baseline — default action, Core Rule Set state, and paranoia level — that every namespace inherits unless it overrides. - -`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons; you can create as many as you need. - -## Inheritance: namespaces override, they don't restart - -A `WAFPolicy` does not have to restate the whole baseline. Its `action` and `coreRuleSet` fields are optional — when unset, the namespace inherits the value from `GlobalWAFPolicy`. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block # override the global default for this namespace - coreRuleSet: - paranoiaLevel: 2 # stricter than the baseline - # coreRuleSet.state is unset → inherited from GlobalWAFPolicy -``` - -| `WAFPolicy` field | When set | When unset | -| :--- | :--- | :--- | -| `action` | overrides the global `defaultAction` for this namespace | inherits `GlobalWAFPolicy.spec.defaultAction` | -| `coreRuleSet.state` | overrides the global state | inherits the global state | -| `coreRuleSet.paranoiaLevel` | overrides the global level | inherits the global level | - -## Plugins are additive, not overriding - -Plugins don't override each other — they stack. When a target is configured, the merger applies **all** referenced global plugins first (in `GlobalWAFPolicy.spec.plugins[]` order), then all referenced namespace plugins (in `WAFPolicy.spec.plugins[]` order). Order is array position; there is no priority field. - -This is what lets a cluster operator's baseline rules and an application operator's exceptions coexist in a single ruleset. The exact interleaving — and why the global `after` slot runs last — is covered in [Merge order and enforcement](merge-and-enforcement.mdx). - -## Validation differs by scope - -Validation is the one place where the two scopes behave differently rather than symmetrically: - -- `GlobalWAFValidationPolicy` can **enforce** (block a non-compliant configuration) or **audit** (warn only), and uses a `namespaceSelector` to choose which namespaces it governs. -- `WAFValidationPolicy` is **advisory only**. It reports violations on the policy status so the AO can self-correct, but it never blocks. Enforcement is a cluster-operator responsibility. diff --git a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx b/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx deleted file mode 100644 index ed116cf26a..0000000000 --- a/calico-enterprise/threat/gateway-waf/concepts/merge-and-enforcement.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -description: How Gateway WAF assembles the merged ruleset, the order rules load in, and the two layers that keep configurations safe — the admission webhook and validation policies. ---- - -# Merge order and enforcement - -When a `WAFPolicy` attaches to a target, the controller gathers every policy and plugin that applies, merges them into a single ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each entry in `targetRefs[]` produces its own `EnvoyExtensionPolicy`, so a policy attached to three routes generates three filters. - -Three things govern the outcome: the **merge order** (how the ruleset is assembled), the **admission webhook** (what raw SecLang you're allowed to submit), and **validation policies** (whether the merged result is compliant). - -## The merged ruleset load order - -Directive order is not cosmetic — the Core Rule Set requires configuration variables to be set before the ruleset loads, and post-processing to run after. The merger assembles directives in this fixed order: - -**Engine setup** -1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. -2. Engine settings — audit logging and request-body access. - -**Plugin `config` (variable setup)** -3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. -4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. - -**Core Rule Set setup** -5. Core Rule Set setup — reads the `tx.*` variables set above. -6. Blocking paranoia level, from the merged paranoia level. -7. Detection paranoia level (always ≥ blocking level). - -**Pre-Core Rule Set rules (`before`)** -8. Global plugin `before` rules. -9. Namespace plugin `before` rules. - -**Core Rule Set** -10. The full OWASP Core Rule Set. - -**Custom rules (`rules`)** -11. Global plugin `rules`. -12. Namespace plugin `rules`. - -**Post-processing (`after`)** -13. Namespace plugin `after` rules. -14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. - -The pattern is global-before-namespace through the early slots, and namespace-before-global in the `after` slot. Within each plugin, its own slots always run in `config` → `before` → `rules` → `after` order. - -:::note - -When two `WAFPolicy` resources target the same route, $[prodname] follows the Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) default: the policy with the oldest `creationTimestamp` wins (ties broken by `namespace/name`). The loser is marked `Accepted=False` with reason `Conflicted`, and its directives are not merged. - -::: - -## Structural safety: the admission webhook - -Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at the moment you apply it. It parses each directive and rejects the resource if any directive fails one of three checks: - -| Check | Passes when | Rejected with | -| :--- | :--- | :--- | -| Directive is allowed | the directive type is on the allowlist (`SecRule`, `SecAction`, `SecRuleRemoveById`, and other safe directives) | `UnsafeDirective` | -| Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | -| Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | - -This is what stops one namespace from colliding with the rule IDs of another, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived and which rule IDs are protected. - -## Validation and enforcement - -The admission webhook checks *raw SecLang*. Validation policies check the *merged result* — and they run after the merge precisely because some problems are only visible there. Two innocent plugins can combine to disable all SQL-injection rules; no admission-time check can see that, but a Rego rule evaluated against the merged directive list can. - -Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with a severity of `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: - -| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | -| :--- | :--- | :--- | :--- | -| `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | -| `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | -| `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | - -To write the rules themselves, see the [Rego validation reference](../reference/rego-validation.mdx). To read the outcome, see [Status conditions](../reference/status-conditions.mdx). diff --git a/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx b/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx deleted file mode 100644 index 38add99b2c..0000000000 --- a/calico-enterprise/threat/gateway-waf/concepts/policies-plugins-validation.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: Understand the three Gateway WAF resource families — policies, plugins, and validation policies — and when to use each. ---- - -# Policies, plugins, and validation - -Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group. They come as three families, and each family has a cluster-scoped resource (owned by the cluster operator) and a namespace-scoped resource (owned by the application operator). - -| Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | -| :--- | :--- | :--- | :--- | -| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, Core Rule Set state, paranoia, which plugins | -| **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | -| **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | - -## Policies — what to enforce - -A **policy** declares the high-level WAF configuration: the action mode (`Detect` or `Block`), whether the OWASP Core Rule Set is enabled, the paranoia level, and which plugins to load. - -- `GlobalWAFPolicy` is a cluster-wide singleton (`name: default`) that sets the baseline for every namespace. -- `WAFPolicy` is namespace-scoped. It attaches WAF to a Gateway or `HTTPRoute` through `targetRefs`, and can override the baseline action and paranoia for that namespace. - -Reach for a policy when you want to **turn WAF on for some traffic** and set its overall posture. See [Attach WAF to a route](../how-to/attach-waf-to-a-route.mdx). - -## Plugins — custom rules - -A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. - -- `GlobalWAFPlugin` holds organization-wide rules — security baselines, compliance rules. -- `WAFPlugin` holds application-specific rules and false-positive exceptions. - -Reach for a plugin when the Core Rule Set isn't enough — you need a **custom detection rule, a pre-processing step, or an exception** for a known false positive. See [Write custom rules](../how-to/write-custom-rules.mdx). - -## Validation policies — guardrails - -A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules that are evaluated against the *merged, effective* WAF configuration. They answer "is this configuration acceptable?" — for example, "the Core Rule Set must stay enabled" or "production namespaces must run in Block mode." - -- `GlobalWAFValidationPolicy` is enforced by the cluster operator. In `Enforce` mode a `Critical` violation blocks the configuration from being programmed; in `Audit` mode it only warns. A `namespaceSelector` scopes which namespaces it applies to. -- `WAFValidationPolicy` is **advisory only** — it surfaces violations on the policy status for the application operator to act on, but never blocks. There is no enforce mode at namespace scope. - -Reach for a validation policy when you want to **codify a requirement** rather than a rule. See [Validate configuration](../how-to/validate-config.mdx) and the [Rego validation reference](../reference/rego-validation.mdx). - -:::note - -Validation policies are not the same as the admission webhook. The webhook checks the *raw SecLang* you submit (safe directives, allowed rule IDs) at write time; validation policies check the *merged result* for compliance. See [Merge order and enforcement](merge-and-enforcement.mdx). - -::: - -## How they fit together - -These resources combine before they reach the data plane: all applicable policies and plugins are merged into one ordered ruleset, validated, and programmed into Envoy Gateway. To understand precedence and inheritance, read: - -- [Global vs. namespaced](global-vs-namespaced.mdx) — scope, ownership, and inheritance. -- [Merge order and enforcement](merge-and-enforcement.mdx) — how the merged ruleset is assembled and checked. diff --git a/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx b/calico-enterprise/threat/gateway-waf/get-started.mdx similarity index 64% rename from calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx rename to calico-enterprise/threat/gateway-waf/get-started.mdx index 42b967d641..73c33236e6 100644 --- a/calico-enterprise/threat/gateway-waf/get-started/quickstart.mdx +++ b/calico-enterprise/threat/gateway-waf/get-started.mdx @@ -1,8 +1,8 @@ --- -description: Deploy a test app, attach Gateway WAF, and confirm it blocks a SQL injection and XSS attack in about five minutes. +description: Enable Gateway WAF on the GatewayAPI operator CR, then deploy a test app and block a SQL injection and XSS attack in about five minutes. --- -# Quickstart +# Get started :::note @@ -10,27 +10,82 @@ This feature is tech preview. Tech preview features may be subject to significan ::: -This guide walks you from a running Ingress Gateway to a blocked attack in about five minutes. +This guide takes you from a running Ingress Gateway to a blocked attack in about five minutes. ## Before you begin **Prerequisites:** -- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- $[prodname] is installed with a valid license that includes the Gateway WAF feature. +- $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). - You have a namespace where you can deploy a test application. -:::tip +:::note -This quickstart uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the tradeoffs. +Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires the Ingress Gateway that $[prodname] manages. ::: -## Step 1: Deploy a test application +## Step 1: Enable the feature -Create an `httpbin` namespace with a deployment and service to act as the backend. +Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. + +```yaml +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled # default is Disabled +``` + +Apply it: ```bash kubectl apply -f - <<'EOF' +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled +EOF +``` + +The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. + +Confirm the six CRDs are installed: + +```bash +kubectl get crd | grep applicationlayer.projectcalico.org +``` + +```text +globalwafplugins.applicationlayer.projectcalico.org +globalwafpolicies.applicationlayer.projectcalico.org +globalwafvalidationpolicies.applicationlayer.projectcalico.org +wafplugins.applicationlayer.projectcalico.org +wafpolicies.applicationlayer.projectcalico.org +wafvalidationpolicies.applicationlayer.projectcalico.org +``` + +All six CRDs must be present before you create any WAF resources. + +:::tip + +To confirm the admission webhook is active, run `kubectl get validatingwebhookconfigurations | grep waf`. You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. + +::: + +## Step 2: Deploy a test application + +Create an `httpbin` namespace with a deployment and service to act as the backend. + +```yaml apiVersion: v1 kind: Namespace metadata: @@ -69,19 +124,17 @@ spec: - protocol: TCP port: 80 targetPort: 80 -EOF ``` -Wait for the pod to be ready: +Apply it, then wait for the pod to be ready: ```bash kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s ``` -## Step 2: Create a Gateway and HTTPRoute +## Step 3: Create a Gateway and HTTPRoute -```bash -kubectl apply -f - <<'EOF' +```yaml apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -110,7 +163,6 @@ spec: backendRefs: - name: httpbin port: 80 -EOF ``` :::note @@ -119,20 +171,19 @@ Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector in ::: -Wait for the Gateway to be accepted: +Apply it, then wait for the Gateway to be accepted: ```bash kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s ``` -## Step 3: Confirm traffic flows +## Step 4: Confirm traffic flows Find the Envoy service that the Ingress Gateway created for this Gateway. Services are created in the `tigera-gateway` namespace. ```bash export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') -echo "Gateway service: $GATEWAY_SVC" ``` Send a test request from inside the cluster: @@ -149,12 +200,17 @@ kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/cur Traffic is flowing through the Gateway with no WAF protection yet. -## Step 4: Apply a GlobalWAFPolicy +## Step 5: Apply a GlobalWAFPolicy + +:::tip + +This quickstart uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [How it works](./how-it-works.mdx#merge-order-and-enforcement) for the tradeoffs. + +::: `GlobalWAFPolicy` sets the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode. -```bash -kubectl apply -f - <<'EOF' +```yaml apiVersion: applicationlayer.projectcalico.org/v3 kind: GlobalWAFPolicy metadata: @@ -164,17 +220,15 @@ spec: coreRuleSet: state: Enabled paranoiaLevel: 1 -EOF ``` This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. -## Step 5: Attach WAF to the Gateway +## Step 6: Attach WAF to the Gateway Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway. -```bash -kubectl apply -f - <<'EOF' +```yaml apiVersion: applicationlayer.projectcalico.org/v3 kind: WAFPolicy metadata: @@ -185,7 +239,6 @@ spec: - group: gateway.networking.k8s.io kind: Gateway name: httpbin-gateway -EOF ``` The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. @@ -207,11 +260,11 @@ Ready=True (Ready) All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. -If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](../troubleshooting.mdx). +If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](./troubleshooting.mdx). Allow a few seconds for Envoy to load the WASM filter before running the tests below. -## Step 6: Test WAF blocking +## Step 7: Test WAF blocking **Normal request — expect 200:** @@ -258,8 +311,11 @@ kubectl delete namespace httpbin kubectl delete globalwafpolicies default ``` +## Troubleshoot + +If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](./troubleshooting.mdx) for diagnosis steps and the pull-secret configuration. + ## Next steps -- [Write custom rules](../how-to/write-custom-rules.mdx) — add application-specific rules with `WAFPlugin`. -- [Validate configuration](../how-to/validate-config.mdx) — codify security requirements with Rego validation policies. -- [Multi-tenant setup](../how-to/multi-tenant-setup.mdx) — cluster operator and application operator workflow with per-namespace governance. +- [Common tasks](./common-tasks.mdx) — set a cluster-wide baseline, write custom rules, validate configuration, and run a full multi-tenant setup. +- [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. diff --git a/calico-enterprise/threat/gateway-waf/get-started/enable.mdx b/calico-enterprise/threat/gateway-waf/get-started/enable.mdx deleted file mode 100644 index 2d6661e4c7..0000000000 --- a/calico-enterprise/threat/gateway-waf/get-started/enable.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -description: Enable Gateway WAF by setting a single field on the GatewayAPI operator CR, which installs the six WAF CRDs and the admission webhook. ---- - -# Enable Gateway WAF - -:::note - -This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. - -::: - -## Before you begin - -**Prerequisites:** - -- $[prodname] is installed with a valid license that includes the Gateway WAF feature. -- $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). - -:::note - -Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires the Ingress Gateway that $[prodname] manages. - -::: - -## Enable the feature - -Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. - -```yaml -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled # default is Disabled -``` - -Apply it: - -```bash -kubectl apply -f - <<'EOF' -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled -EOF -``` - -The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. - -## Verify - -Confirm the six CRDs are installed: - -```bash -kubectl get crd | grep applicationlayer.projectcalico.org -``` - -```text -globalwafplugins.applicationlayer.projectcalico.org -globalwafpolicies.applicationlayer.projectcalico.org -globalwafvalidationpolicies.applicationlayer.projectcalico.org -wafplugins.applicationlayer.projectcalico.org -wafpolicies.applicationlayer.projectcalico.org -wafvalidationpolicies.applicationlayer.projectcalico.org -``` - -All six CRDs must be present before you create any WAF resources. - -Next, create a baseline `GlobalWAFPolicy` and confirm $[prodname] accepts it. See [Set a baseline](../how-to/set-a-baseline.mdx). - -:::tip - -To confirm the admission webhook is active, run: - -```bash -kubectl get validatingwebhookconfigurations | grep waf -``` - -You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. - -::: - -## Troubleshoot - -If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](../troubleshooting.mdx) for diagnosis steps and the pull-secret configuration. - -## Next steps - -- [Quickstart](./quickstart.mdx) — block an attack in under five minutes. -- [Set a baseline](../how-to/set-a-baseline.mdx) — create the cluster-wide `GlobalWAFPolicy`. -- [Policies, plugins, and validation](../concepts/policies-plugins-validation.mdx) — understand the six resource types. diff --git a/calico-enterprise/threat/gateway-waf/how-it-works.mdx b/calico-enterprise/threat/gateway-waf/how-it-works.mdx new file mode 100644 index 0000000000..3d59841fe4 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/how-it-works.mdx @@ -0,0 +1,159 @@ +--- +description: How Gateway WAF resources are organized, how cluster-scoped and namespace-scoped configuration relate, and how the merged ruleset is assembled and enforced. +--- + +# How it works + +Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group, arranged as three families. Each family has a cluster-scoped resource, owned by the **cluster operator (CO)**, and a namespace-scoped resource, owned by the **application operator (AO)**. + +| Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | +| :--- | :--- | :--- | :--- | +| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, Core Rule Set state, paranoia, which plugins | +| **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | +| **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | + +The cluster operator *sets* the security posture; the application operator *tunes* it for their workloads. An AO can make their namespace stricter, and can be prevented from making it weaker through validation policies (see [Validation and enforcement](#validation-and-enforcement)). + +## Policies — what to enforce + +A **policy** declares the high-level WAF configuration: the action mode (`Detect` or `Block`), whether the OWASP Core Rule Set is enabled, the paranoia level, and which plugins to load. + +- `GlobalWAFPolicy` is a cluster-wide singleton (`name: default`) that sets the baseline for every namespace. +- `WAFPolicy` is namespace-scoped. It attaches WAF to a Gateway or `HTTPRoute` through `targetRefs`, and can override the baseline action and paranoia for that namespace. + +Reach for a policy when you want to turn WAF on for some traffic and set its overall posture. + +## Plugins — custom rules + +A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. + +- `GlobalWAFPlugin` holds organization-wide rules — security baselines, compliance rules. +- `WAFPlugin` holds application-specific rules and false-positive exceptions. + +Reach for a plugin when the Core Rule Set isn't enough — you need a custom detection rule, a pre-processing step, or an exception for a known false positive. + +## Validation policies — guardrails + +A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules that are evaluated against the *merged, effective* WAF configuration. They answer "is this configuration acceptable?" — for example, "the Core Rule Set must stay enabled" or "production namespaces must run in Block mode." + +- `GlobalWAFValidationPolicy` is enforced by the cluster operator. In `Enforce` mode a `Critical` violation blocks the configuration from being programmed; in `Audit` mode it only warns. A `namespaceSelector` scopes which namespaces it applies to. +- `WAFValidationPolicy` is advisory only — it surfaces violations on the policy status for the application operator to act on, but never blocks. There is no enforce mode at namespace scope. + +Reach for a validation policy when you want to codify a requirement rather than a rule. + +:::note + +Validation policies are not the same as the admission webhook. The webhook checks the *raw SecLang* you submit (safe directives, allowed rule IDs) at write time; validation policies check the *merged result* for compliance. See [Validation and enforcement](#validation-and-enforcement) below. + +::: + +## The global policy is a singleton + +`GlobalWAFPolicy` is a cluster-wide singleton: it must be named `default`. It establishes the cluster baseline — default action, Core Rule Set state, and paranoia level — that every namespace inherits unless it overrides. + +`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons; you can create as many as you need. + +## Inheritance: namespaces override, they don't restart + +A `WAFPolicy` does not have to restate the whole baseline. Its `action` and `coreRuleSet` fields are optional — when unset, the namespace inherits the value from `GlobalWAFPolicy`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override the global default for this namespace + coreRuleSet: + paranoiaLevel: 2 # stricter than the baseline + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +| `WAFPolicy` field | When set | When unset | +| :--- | :--- | :--- | +| `action` | overrides the global `defaultAction` for this namespace | inherits `GlobalWAFPolicy.spec.defaultAction` | +| `coreRuleSet.state` | overrides the global state | inherits the global state | +| `coreRuleSet.paranoiaLevel` | overrides the global level | inherits the global level | + +## Plugins are additive, not overriding + +Plugins don't override each other — they stack. When a target is configured, the merger applies **all** referenced global plugins first (in `GlobalWAFPolicy.spec.plugins[]` order), then all referenced namespace plugins (in `WAFPolicy.spec.plugins[]` order). Order is array position; there is no priority field. + +This is what lets a cluster operator's baseline rules and an application operator's exceptions coexist in a single ruleset. The exact interleaving — and why the global `after` slot runs last — is covered below. + +## Merge order and enforcement + +When a `WAFPolicy` attaches to a target, the controller gathers every policy and plugin that applies, merges them into a single ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each entry in `targetRefs[]` produces its own `EnvoyExtensionPolicy`, so a policy attached to three routes generates three filters. + +Three things govern the outcome: the **merge order** (how the ruleset is assembled), the **admission webhook** (what raw SecLang you're allowed to submit), and **validation policies** (whether the merged result is compliant). + +### The merged ruleset load order + +Directive order is not cosmetic — the Core Rule Set requires configuration variables to be set before the ruleset loads, and post-processing to run after. The merger assembles directives in this fixed order: + +**Engine setup** +1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. +2. Engine settings — audit logging and request-body access. + +**Plugin `config` (variable setup)** +3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. +4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. + +**Core Rule Set setup** +5. Core Rule Set setup — reads the `tx.*` variables set above. +6. Blocking paranoia level, from the merged paranoia level. +7. Detection paranoia level (always ≥ blocking level). + +**Pre-Core Rule Set rules (`before`)** +8. Global plugin `before` rules. +9. Namespace plugin `before` rules. + +**Core Rule Set** +10. The full OWASP Core Rule Set. + +**Custom rules (`rules`)** +11. Global plugin `rules`. +12. Namespace plugin `rules`. + +**Post-processing (`after`)** +13. Namespace plugin `after` rules. +14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. + +The pattern is global-before-namespace through the early slots, and namespace-before-global in the `after` slot. Within each plugin, its own slots always run in `config` → `before` → `rules` → `after` order. + +:::note + +When two `WAFPolicy` resources target the same route, $[prodname] follows the Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) default: the policy with the oldest `creationTimestamp` wins (ties broken by `namespace/name`). The loser is marked `Accepted=False` with reason `Conflicted`, and its directives are not merged. + +::: + +### Structural safety: the admission webhook + +Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at the moment you apply it. It parses each directive and rejects the resource if any directive fails one of three checks: + +| Check | Passes when | Rejected with | +| :--- | :--- | :--- | +| Directive is allowed | the directive type is on the allowlist (`SecRule`, `SecAction`, `SecRuleRemoveById`, and other safe directives) | `UnsafeDirective` | +| Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | +| Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | + +This is what stops one namespace from colliding with the rule IDs of another, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for how the range is derived and which rule IDs are protected. + +### Validation and enforcement + +The admission webhook checks *raw SecLang*. Validation policies check the *merged result* — and they run after the merge precisely because some problems are only visible there. Two innocent plugins can combine to disable all SQL-injection rules; no admission-time check can see that, but a Rego rule evaluated against the merged directive list can. + +Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with a severity of `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: + +| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | +| :--- | :--- | :--- | :--- | +| `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | +| `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | +| `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | + +To write the rules themselves, see [Rego validation](./reference.mdx#rego-validation). To read the outcome, see [Status conditions](./reference.mdx#status-conditions). diff --git a/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx b/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx deleted file mode 100644 index 23f167c919..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-to/attach-waf-to-a-route.mdx +++ /dev/null @@ -1,227 +0,0 @@ ---- -description: Create a WAFPolicy with targetRefs to attach WAF inspection to a Gateway or HTTPRoute in your namespace. ---- - -# Attach WAF to a Gateway or route - -:::note[Application operator task] - -This page is for application operators who own namespace-scoped WAF resources and decide which of their Gateways and routes receive WAF inspection. - -::: - -WAF inspection does not start automatically when the cluster operator enables the feature. You activate it by creating a `WAFPolicy` in your namespace that points to one or more Gateway API targets. The policy inherits the cluster baseline and lets you layer namespace-specific overrides on top. - -## Prerequisites - -- The cluster operator has created a `GlobalWAFPolicy`. See [Set a cluster-wide baseline](./set-a-baseline.mdx). -- You have write access to `WAFPolicy` resources in your namespace. -- A `Gateway` or `HTTPRoute` in your namespace is already receiving traffic. See [About Calico Ingress Gateway](../../../networking/ingress-gateway/about-calico-ingress-gateway.mdx) if you have not set one up. - -## Understand the two attachment scopes - -`WAFPolicy.spec.targetRefs[]` accepts two kinds of targets. - -| `kind` in `targetRefs` | WAF covers | Use when | -| :--- | :--- | :--- | -| `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | -| `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | - -Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` on Envoy Gateway. A policy with two entries generates two filters — one per target. - -:::note - -`sectionName` (targeting a specific listener on a Gateway) is reserved for a future release. Omit it. - -::: - -## Step 1 — Create a WAFPolicy - -### Option A — Gateway-wide WAF - -This example attaches WAF to every route on the `team-gateway` Gateway. Because `action` and `coreRuleSet` are unset, both fields inherit the cluster baseline. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: team-gateway-waf - namespace: team-a -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: team-gateway -``` - -### Option B — Per-route WAF - -This example attaches WAF to a single `HTTPRoute` and overrides the action and paranoia level for this namespace. Unset fields (here, `coreRuleSet.state`) still inherit from the cluster baseline. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-api-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block # override: Block even if global baseline is Detect - coreRuleSet: - paranoiaLevel: 2 # stricter than the cluster default - # coreRuleSet.state is unset → inherited from GlobalWAFPolicy -``` - -### Combining both scopes in one policy - -You can list both a Gateway and specific HTTPRoutes in a single `WAFPolicy`: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: mixed-targets - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: payments-gateway - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: internal-api - action: Block -``` - -Up to 16 entries are allowed in `targetRefs`. - -Apply the policy: - -```bash -kubectl apply -f wafpolicy.yaml -``` - -## Step 2 — Add a namespace plugin (optional) - -If you need rules beyond the cluster baseline — such as a custom detection rule or a false-positive exception — create a `WAFPlugin` and reference it from `WAFPolicy.spec.plugins[]`: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: payments-rules - namespace: payments -spec: - description: "Payments-specific WAF rules" - rules: | - SecRule REQUEST_HEADERS:Content-Type "!@rx ^application/json" \ - "id:5600001,phase:1,deny,status:415,msg:'Only JSON accepted on payments API'" ---- -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-api-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block - plugins: - - kind: WAFPlugin - name: payments-rules -``` - -Rule IDs must fall within the ID range allocated to your namespace. See [Write custom rules](./write-custom-rules.mdx) for how to find your range and how the four plugin slots work. - -## Inheritance and field overrides - -Fields you set in `WAFPolicy` override the corresponding field in `GlobalWAFPolicy`. Fields you leave unset are inherited. - -| `WAFPolicy` field | When set | When unset | -| :--- | :--- | :--- | -| `action` | overrides `GlobalWAFPolicy.spec.defaultAction` | inherits the global default | -| `coreRuleSet.state` | overrides the global Core Rule Set state | inherits the global Core Rule Set state | -| `coreRuleSet.paranoiaLevel` | overrides the global paranoia level | inherits the global paranoia level | - -For the complete picture, including how global and namespace plugins stack, see [Global vs. namespaced](../concepts/global-vs-namespaced.mdx). - -## Conflict resolution - -Only one `WAFPolicy` may be the active policy for any given target at a time. If two `WAFPolicy` resources in the same namespace both target the same Gateway or HTTPRoute, $[prodname] follows the [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) tie-breaking rule: - -- The policy with the **oldest `creationTimestamp`** wins. -- The newer policy is marked `Accepted=False` with reason `Conflicted`. -- Only the winning policy's directives are merged and programmed. - -To resolve a conflict, delete the newer policy or update its `targetRefs` to point at a different target. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the full conflict model. - -## Step 3 — Verify the policy - -Check that the policy was accepted and programmed: - -```bash -kubectl get wafpolicy payments-api-waf -n payments -``` - -``` -NAME ACCEPTED PROGRAMMED AGE -payments-api-waf True True 45s -``` - -For detailed conditions — including validation violations or conflict reasons — inspect the full status: - -```bash -kubectl get wafpolicy payments-api-waf -n payments -o yaml -``` - -The `.status.conditions` field reports `Accepted`, `Programmed`, and any validation findings. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. - -You can also confirm that Envoy Gateway received the filter: - -```bash -kubectl get envoyextensionpolicies -n payments -``` - -Each `targetRefs` entry produces one `EnvoyExtensionPolicy`. A policy with two entries will show two resources. - -## Step 4 — Test the WAF - -Send a request that includes a classic SQL injection pattern. In `Block` mode this should return HTTP 403: - -```bash -curl -si "https:///api?id=1+OR+1%3D1" -``` - -``` -HTTP/1.1 403 Forbidden -content-length: 11 -content-type: text/plain - -Access denied -``` - -:::tip - -If the Gateway is running in `Detect` mode (the cluster default until you upgrade to `Block`), the request will be allowed through but the WAF event will appear in the Envoy Gateway logs. Check with: - -```bash -kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "waf\|coraza\|modsec" -``` - -::: - -:::caution - -If you get `ACCEPTED=False` with reason `ValidationFailed`, a `GlobalWAFValidationPolicy` is blocking your configuration. The status message will identify which policy and rule triggered the violation. Contact your cluster operator or see [Validate configuration](./validate-config.mdx) for the self-check workflow. - -::: - -## Next steps - -- **Add app-specific rules or exceptions** — write custom ModSecurity rules or suppress false positives with a `WAFPlugin`. See [Write custom rules](./write-custom-rules.mdx). -- **Self-validate your config** — check compliance against the cluster operator's requirements before they enforce it. See [Validate configuration](./validate-config.mdx). diff --git a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx deleted file mode 100644 index 8dfbeaa5eb..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-to/multi-tenant-setup.mdx +++ /dev/null @@ -1,284 +0,0 @@ ---- -description: Walk through an end-to-end Gateway WAF multi-tenant setup where a cluster operator sets a cluster-wide baseline and two application teams tune WAF independently within enforced guardrails. ---- - -# Multi-tenant setup - -:::note[Scenario] - -This guide walks you through a single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. You will see the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. - -Prerequisites: Gateway WAF is enabled and a valid license is in place. See [Enable Gateway WAF](../get-started/enable.mdx). - -::: - ---- - -## Part 1: Cluster operator - -All resources in this section are created by the **cluster operator** using cluster-scoped credentials. - -### Set the baseline policy - -`GlobalWAFPolicy` is a singleton named `default`. It defines the security floor every namespace inherits. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Block - coreRuleSet: - state: Enabled - paranoiaLevel: 1 - plugins: - - name: org-security-baseline -``` - -This establishes Block mode and OWASP Core Rule Set at paranoia level 1 for every namespace. A `WAFPolicy` can raise the paranoia level or override the action, but the validation policy below prevents it from lowering either. - -### Add an org-wide plugin - -`GlobalWAFPlugin` resources carry custom rules that apply everywhere. The plugin referenced above adds a request-tracing requirement: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPlugin -metadata: - name: org-security-baseline -spec: - description: "Org-wide rules — requires a correlation ID on every request" - rules: | - SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ - "id:800010,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" - after: | - SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ - "id:800099,phase:5,pass,log,msg:'WAF blocked request'" -``` - -The `after` slot runs last in the merge order, giving the cluster operator final control over anomaly scoring and audit logging. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the full load sequence. - -### Enforce compliance guardrails - -`GlobalWAFValidationPolicy` uses Rego rules to evaluate every `WAFPolicy` after the merge. Setting `mode: Enforce` means a `Critical` violation blocks programming — no Coraza filter is loaded — until the namespace policy is corrected. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: production-requirements -spec: - mode: Enforce - namespaceSelector: - matchLabels: - env: production - rules: - - name: must-use-block-mode - severity: Critical - rego: | - package waf.validation - violation[msg] { - input.config.action != "Block" - msg := "WAF policy must use Block mode in production namespaces" - } - - name: crs-must-be-enabled - severity: Critical - rego: | - package waf.validation - violation[msg] { - input.config.crsState != "Enabled" - msg := "OWASP CRS must be enabled in production namespaces" - } - - name: minimum-paranoia-level - severity: Warning - rego: | - package waf.validation - violation[msg] { - input.config.paranoiaLevel < 1 - msg := "Paranoia level 1 is the recommended minimum" - } -``` - -The `namespaceSelector` targets only namespaces labelled `env: production`. The two `Critical` rules are hard gates; a `Warning` appears in `status.validation.failures` but does not block programming. For Rego variable reference, see [Rego validation](../reference/rego-validation.mdx). - -### Give application teams read access to global resources - -Application operators need to read `Global*` resources to understand what baseline they are inheriting. Grant read access with a `ClusterRole` and a per-namespace `RoleBinding`: - -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: waf-global-reader -rules: - - apiGroups: ["applicationlayer.projectcalico.org"] - resources: - - globalwafpolicies - - globalwafplugins - - globalwafvalidationpolicies - verbs: ["get", "list", "watch"] -``` - -Bind it in each team's namespace: - -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: waf-global-reader - namespace: team-a # repeat for payments -subjects: - - kind: Group - name: team-a-operators - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: ClusterRole - name: waf-global-reader - apiGroup: rbac.authorization.k8s.io -``` - -:::note - -This grants read on cluster-scoped resources via a namespace-scoped `RoleBinding`. Application operators can see the baseline and validation policy, but cannot create or edit `Global*` resources. - -::: - ---- - -## Part 2: `team-a` operator - -The `team-a` operator attaches WAF to their Gateway and inherits the baseline. Because `coreRuleSet.state` is unset, it inherits `Enabled` from `GlobalWAFPolicy`. The operator raises paranoia to 2 but stays within the enforced guardrails. - -Label the namespace so the validation policy applies: - -```bash -kubectl label namespace team-a env=production -``` - -Create the `WAFPolicy`: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: team-a-waf - namespace: team-a -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: team-a-gateway - action: Block - coreRuleSet: - paranoiaLevel: 2 - # state is unset — inherited from GlobalWAFPolicy (Enabled) -``` - -After reconciliation the policy should reach `Ready=True`. The merged ruleset includes: -1. The org-wide `X-Request-ID` requirement from `GlobalWAFPlugin/org-security-baseline` -2. The OWASP Core Rule Set at paranoia level 2 (overriding the global level 1) -3. The `after` audit rule from the global plugin - -Verify the conditions: - -```bash -kubectl describe wafpolicy team-a-waf -n team-a -``` - -See [Status conditions](../reference/status-conditions.mdx) for what each condition means. - ---- - -## Part 3: `payments` operator - -The `payments` team runs a stricter configuration. They raise paranoia to 3 and add a namespace plugin that blocks GraphQL introspection. - -Label the namespace: - -```bash -kubectl label namespace payments env=production -``` - -First, find the namespace's allocated SecRule ID range so custom rule IDs land inside it: - -```bash -kubectl get namespace payments \ - -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' -``` - -The controller stamps this annotation on first use. All rule IDs in the plugin below must fall within this range. See [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for how the range is derived. - -Create the namespace plugin: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: payments-app-rules - namespace: payments -spec: - description: "Payments-specific rules — block GraphQL introspection" - rules: | - SecRule REQUEST_BODY "@rx (?i)__schema|__type" \ - "id:420010,phase:2,deny,status:403,msg:'GraphQL introspection blocked'" -``` - -Replace `420010` with an ID from your namespace's actual allocated range. - -Create the `WAFPolicy` referencing the plugin: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: payments-gateway - action: Block - coreRuleSet: - state: Enabled - paranoiaLevel: 3 - plugins: - - name: payments-app-rules -``` - -### What happens if the payments team tries to weaken the baseline - -If the `payments` operator applies a policy with `action: Detect`, the `production-requirements` `GlobalWAFValidationPolicy` (in `Enforce` mode) fires the `must-use-block-mode` Critical rule. The controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation in `status.validation.failures`: - -```bash -kubectl get wafpolicy payments-waf -n payments \ - -o jsonpath='{.status.validation.failures}' | jq -``` - -No `EnvoyExtensionPolicy` is generated until the policy is corrected. The `payments` operator must restore `action: Block` or request a policy exception from the cluster operator. See [Validate config](./validate-config.mdx) for how to read and act on validation failures. - ---- - -## Result: isolation in practice - -Each team's WAF configuration is fully isolated by namespace: - -- **`team-a`** runs Core Rule Set at paranoia 2 with its own custom rules, scoped to its ID range. -- **`payments`** runs Core Rule Set at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. -- Neither team can disable the org-wide `X-Request-ID` rule or lower below Block mode. -- The cluster operator sees all validation violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — a single place to audit which namespaces are out of compliance. - -For the full ID-range isolation mechanism, see [SecRule ID ranges](../reference/secrule-id-ranges.mdx). For the cluster operator's cluster-wide violation view, see [Status conditions](../reference/status-conditions.mdx). - -## Related pages - -- [Concepts: Global vs. namespaced](../concepts/global-vs-namespaced.mdx) -- [Concepts: Merge order and enforcement](../concepts/merge-and-enforcement.mdx) -- [Reference: SecRule ID ranges](../reference/secrule-id-ranges.mdx) -- [Reference: Status conditions](../reference/status-conditions.mdx) -- [How-to: Attach WAF to a route](./attach-waf-to-a-route.mdx) -- [How-to: Write custom rules](./write-custom-rules.mdx) -- [How-to: Set a baseline](./set-a-baseline.mdx) -- [How-to: Validate config](./validate-config.mdx) -- [Troubleshooting](../troubleshooting.mdx) diff --git a/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx b/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx deleted file mode 100644 index ee50514c5c..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-to/set-a-baseline.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -description: Create a GlobalWAFPolicy and GlobalWAFPlugin to establish a cluster-wide WAF security baseline that every namespace inherits. ---- - -# Set a cluster-wide baseline - -:::note[Cluster operator task] - -This page is for cluster operators who own the `GlobalWAFPolicy` singleton and set the security posture that all namespaces inherit. - -::: - -Before any application operator can attach WAF to a route, you need a cluster baseline. The baseline defines the default action, Core Rule Set state, paranoia level, and any organization-wide custom rules. Application operators inherit these settings; they can tighten them, but a [validation policy](./validate-config.mdx) can prevent them from weakening them. - -## Prerequisites - -- The Gateway WAF feature gate is enabled on the `ApplicationLayer` resource. See [Enable Gateway WAF](../get-started/enable.mdx). -- You have cluster-admin rights (the `GlobalWAFPolicy` API is cluster-scoped). - -## Step 1 — Create the GlobalWAFPolicy singleton - -`GlobalWAFPolicy` must be named `default`. Applying any other name is rejected by the admission webhook. - -Start in `Detect` mode. `Detect` logs matched requests but allows them through, giving you time to measure false positives before you switch to `Block`. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Detect # start here; promote to Block after validating false-positive rate - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` - -Apply it: - -```bash -kubectl apply -f globalwafpolicy.yaml -``` - -### Paranoia levels - -The OWASP Core Rule Set defines four paranoia levels. Each higher level adds more detection rules and will catch more attacks — at the cost of more false positives. Choose based on your organization's risk tolerance and the sensitivity of the workloads you are protecting. - -| Level | Posture | Typical use | -| :--- | :--- | :--- | -| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | -| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | -| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | -| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | - -:::tip - -Start at paranoia level 1 cluster-wide. Let application operators raise the level for their own namespaces if their traffic demands it. You can enforce a minimum level later with a [validation policy](./validate-config.mdx). - -::: - -### When to switch to Block - -Once you have confirmed the false-positive rate is acceptable, change `defaultAction` to `Block`: - -```yaml -spec: - defaultAction: Block - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` - -In `Block` mode, $[prodname] returns HTTP 403 for any request that matches a WAF rule. - -## Step 2 — Add organization-wide rules with a GlobalWAFPlugin - -A `GlobalWAFPlugin` holds custom ModSecurity rules that apply to every namespace through the global policy's `plugins[]` list. Create the plugin first, then reference it from `GlobalWAFPolicy`. - -Rules are organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — that map to distinct points in the load order. See [Write custom rules](./write-custom-rules.mdx) for slot semantics and ID-range requirements. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPlugin -metadata: - name: org-baseline -spec: - description: "Organization-wide security baseline rules" - config: | - # Slot 1 — set tx.* variables before CRS loads - SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" - before: | - # Slot 2 — pre-CRS rules; run before the Core Rule Set - SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ - "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" - SecRule TX:ENFORCE_HTTPS "@eq 1" "" - rules: | - # Slot 3 — custom rules that run alongside CRS - SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ - "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" - after: | - # Slot 4 — post-CRS processing; cluster operator's after slot runs last - SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ - "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" -``` - -Apply the plugin: - -```bash -kubectl apply -f globalwafplugin.yaml -``` - -Reference it from `GlobalWAFPolicy.spec.plugins[]`: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Detect - coreRuleSet: - state: Enabled - paranoiaLevel: 1 - plugins: - - kind: GlobalWAFPlugin - name: org-baseline -``` - -`plugins[]` is an ordered list. The merger processes plugins in array position order. Add more `GlobalWAFPlugin` entries to the array as your organization grows — up to 64 total. - -:::note - -You can add `WAFPlugin` entries to `GlobalWAFPolicy.spec.plugins[]` as well. For the complete merge order — including when global `after` slots run — see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx). - -::: - -## Step 3 — Verify the baseline - -Check that the resource was accepted: - -```bash -kubectl get globalwafpolicy default -``` - -``` -NAME ACCEPTED AGE -default True 30s -``` - -For detailed status — including any validation violations — inspect the status conditions: - -```bash -kubectl get globalwafpolicy default -o yaml -``` - -Look at `.status.conditions`. An `Accepted=True` condition with reason `Reconciled` means the baseline is programmed. If a [validation policy](../reference/status-conditions.mdx) blocked the configuration, you will see `Accepted=False` with a `ValidationFailed` reason and a message describing the violation. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. - -## How namespace inheritance works - -Every namespace that has a `WAFPolicy` attached to a target inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy`. A `WAFPolicy` can override any of these fields for its namespace; unset fields fall back to the global value. - -``` -GlobalWAFPolicy (baseline) - └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 - └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy -``` - -For a full account of which fields inherit and which override, see [Global vs. namespaced](../concepts/global-vs-namespaced.mdx). - -## Next steps - -- **Enforce compliance** — codify your requirements (minimum paranoia level, Block-only production) with a `GlobalWAFValidationPolicy`. See [Validate configuration](./validate-config.mdx). -- **Let teams attach WAF** — share [Attach WAF to a Gateway or route](./attach-waf-to-a-route.mdx) with your application operators. -- **Multi-tenant setup** — if you manage many teams, see [Multi-tenant setup](./multi-tenant-setup.mdx) for namespace selector patterns and ID-range allocation. diff --git a/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx b/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx deleted file mode 100644 index c6d0f13e06..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-to/validate-config.mdx +++ /dev/null @@ -1,257 +0,0 @@ ---- -description: Write Rego validation rules to audit or enforce organizational WAF standards across namespaces, and read violation results from resource status. ---- - -# Validate configuration with Rego - -Gateway WAF has two guardrail layers. The admission webhook checks raw SecLang at apply time (structural safety). Validation policies check the **merged result** at reconcile time — after all policies and plugins have been combined — so they can catch problems that are only visible in the assembled directive list. - -Validation rules are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), evaluated against a structured input document that describes the effective configuration. This page shows you how to write and apply them. For the full input schema and the violations contract, see [Rego validation](../reference/rego-validation.mdx). - -## How enforcement works - -There are two validation resources, with different scope and enforcement power: - -| Resource | Scope | Can block? | Persona | -| :--- | :--- | :--- | :--- | -| `GlobalWAFValidationPolicy` | Cluster-wide (optional `namespaceSelector`) | Yes — in `Enforce` mode with `Critical` severity | Cluster operator | -| `WAFValidationPolicy` | Namespace | No — advisory only | Application operator | - -The enforcement matrix is in [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#validation-and-enforcement). - ---- - -## Cluster operator: GlobalWAFValidationPolicy - -:::note[Cluster operator] - -`GlobalWAFValidationPolicy` is a cluster-scoped resource. Only cluster operators can create or modify it. It applies to every namespace, or to a label-selected subset via `namespaceSelector`. - -::: - -A `GlobalWAFValidationPolicy` carries a list of Rego rules and an `enforcementMode`: - -- `Audit` (default) — violations are recorded on status, but the WAF filter is still programmed. -- `Enforce` — a `Critical` violation blocks the filter from being programmed. `Warning` and `Info` violations are recorded on status regardless of mode. - -### Example: organizational security requirements - -The following policy enforces three requirements for all namespaces labeled `env: production`, and audits a fourth: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: production-requirements -spec: - enforcementMode: Enforce - namespaceSelector: - matchLabels: - env: production - rules: - - # Critical: CRS must be enabled - - name: crs-must-be-enabled - severity: Critical - rego: | - package waf - violations[{"msg": "OWASP CRS must be enabled"}] { - input.config.crsState != "Enabled" - } - - # Critical: production namespaces must use Block mode - - name: block-mode-required - severity: Critical - rego: | - package waf - violations[{"msg": "WAF must be in Block mode for production"}] { - input.config.action != "Block" - } - - # Critical: minimum paranoia level - - name: min-paranoia-level - severity: Critical - rego: | - package waf - violations[{"msg": msg}] { - input.config.paranoiaLevel < 2 - msg := sprintf( - "paranoia level %d is below the production minimum of 2", - [input.config.paranoiaLevel] - ) - } - - # Warning: advise against disabling RCE rules (does not block) - - name: rce-rules-intact - severity: Warning - rego: | - package waf - violations[{"msg": msg}] { - d := input.directives[_] - startswith(d, "SecRuleRemoveById") - re_match(`93[0-9]{4}`, d) - msg := sprintf("RCE rule removal detected in merged config: %s", [d]) - } -``` - -:::tip - -Start every new `GlobalWAFValidationPolicy` in `Audit` mode. Review the violations that appear on existing `WAFPolicy` status before switching to `Enforce`. This avoids blocking teams whose configurations predate the new policy. - -::: - -### Scoping with namespaceSelector - -Leave `namespaceSelector` empty (or omit it) to apply the policy to every namespace. Use it to target a subset: - -```yaml -spec: - namespaceSelector: - matchLabels: - compliance: pci-dss # only namespaces with this label -``` - -Multiple `GlobalWAFValidationPolicy` objects can coexist. Every matching policy's rules are evaluated; a single `Critical` violation from any of them (when in `Enforce` mode) is enough to block programming. - -### PCI-DSS example - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: pci-dss-requirements -spec: - enforcementMode: Enforce - namespaceSelector: - matchLabels: - compliance: pci-dss - rules: - - - name: pci-crs-enabled - severity: Critical - rego: | - package waf - violations[{"msg": "PCI-DSS requires OWASP CRS to be enabled"}] { - input.config.crsState != "Enabled" - } - - - name: pci-block-mode - severity: Critical - rego: | - package waf - violations[{"msg": "PCI-DSS requires WAF Block mode"}] { - input.config.action != "Block" - } - - - name: pci-no-rule-removal - severity: Critical - rego: | - package waf - violations[{"msg": msg}] { - d := input.directives[_] - startswith(d, "SecRuleRemoveById") - msg := sprintf("PCI-DSS forbids disabling any CRS rules: %s", [d]) - } -``` - ---- - -## Application operator: WAFValidationPolicy - -:::note[Application operator] - -`WAFValidationPolicy` is namespace-scoped. It is advisory only — violations appear on status but never block the WAF filter from being programmed. Use it as a self-check for your namespace's configuration. - -::: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFValidationPolicy -metadata: - name: team-self-check - namespace: platform -spec: - rules: - - - name: graphql-plugin-active - severity: Warning - rego: | - package waf - violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { - not "graphql-waf" in input.source.namespacePlugins - } -``` - -Because `WAFValidationPolicy` is advisory, it is safe to define your own standards here that are stricter than the cluster baseline — for example, requiring a minimum paranoia level higher than the global default. Violations surface on the `WAFPolicy` status and prompt the author to fix the config, but they do not interrupt traffic. - ---- - -## Reading violation results - -Violations from all matching validation policies appear in the status conditions of the affected `WAFPolicy`. Check them with: - -```bash -kubectl get wafpolicy -n -o yaml -``` - -A blocked policy looks like: - -```yaml -status: - conditions: - - type: Ready - status: "False" - reason: ValidationFailed - message: > - GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": - WAF must be in Block mode for production - - type: Accepted - status: "True" -``` - -An audit violation (filter still programmed) looks like: - -```yaml -status: - conditions: - - type: Ready - status: "True" - - type: ValidationWarning - status: "True" - reason: AuditViolation - message: > - GlobalWAFValidationPolicy/production-requirements rule "rce-rules-intact" [Warning]: - RCE rule removal detected in merged config: SecRuleRemoveById 932100 -``` - -See [Status conditions](../reference/status-conditions.mdx) for the complete condition type reference. - ---- - -## Authoring Rego rules - -Each rule in `spec.rules[].rego` must: - -1. Declare `package waf`. -2. Define a `violations` rule that produces a set of objects, each with a `msg` field. - -You can inspect the full directive list to catch combinations that individual fields won't reveal: - -```rego -package waf - -violations[{"msg": msg}] { - d := input.directives[_] - startswith(d, "SecRuleRemoveById") - re_match(`94[0-9]{4}`, d) # targets a SQL-injection CRS rule - msg := sprintf("SQLi rule removal in merged config: %s", [d]) -} -``` - -For the complete input document schema, the violations contract, and pass/fail worked examples, see [Rego validation](../reference/rego-validation.mdx). - -:::tip - -Write your Rego rules against the input schema in [Rego validation](../reference/rego-validation.mdx) and test them locally with `opa eval` before applying to the cluster. A syntax error in a rule causes the entire policy to be marked invalid; the status reports which rule failed to parse. - -::: diff --git a/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx deleted file mode 100644 index 8dac9dc5b4..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-to/write-custom-rules.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -description: Write ModSecurity rules in a WAFPlugin to extend or tune the WAF for your application, and attach the plugin to a WAFPolicy. ---- - -# Write custom rules with plugins - -:::note[Application operator task] - -This page is for **application operators** who manage WAF configuration within a namespace. Cluster operators who want organization-wide rules should create a `GlobalWAFPlugin` and reference it from `GlobalWAFPolicy.spec.plugins[]` — the CRD fields are identical; only scope differs. - -::: - -A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots. The slots map to Core Rule Set 4's plugin file structure and determine when your directives run relative to the OWASP Core Rule Set. - -## The four slots - -The merger assembles every plugin's slots into a fixed load order. Understanding that order is the key to knowing which slot to use. See [Merge order and enforcement](../concepts/merge-and-enforcement.mdx) for the complete sequence. - -### `config` — variable setup - -**Runs before Core Rule Set setup.** Use `config` to set `tx.*` transaction variables that the Core Rule Set reads during its own initialization. Examples: tuning anomaly score thresholds, enabling optional Core Rule Set features, or setting application-specific flags. - -```yaml -config: | - SecAction \ - "id:420001,phase:1,pass,nolog,\ - setvar:tx.paranoia_level=2,\ - setvar:tx.anomaly_score_threshold=5" -``` - -If you write a rule in `config` that fires on a request variable (such as a request header), place it in `before` instead — at `config` time the request has not been read yet. - -### `before` — pre-Core Rule Set rules - -**Runs after variable setup but before the Core Rule Set loads.** Use `before` for rules that should execute before Core Rule Set analysis: early-exit allowlisting for trusted paths, request normalization, or setting flags that later Core Rule Set rules or your own rules can read. - -```yaml -before: | - SecRule REQUEST_URI "@beginsWith /internal/health" \ - "id:420010,phase:1,pass,nolog,ctl:ruleEngine=Off" -``` - -:::caution - -`ctl:ruleEngine=Off` inside a chained rule or as a per-request action disables the engine for that transaction only. Disabling it globally via a top-level `SecRuleEngine Off` directive is not permitted and will be rejected by the admission webhook. - -::: - -### `rules` — custom rules alongside Core Rule Set - -**Runs after the full Core Rule Set has executed.** This is a Tigera extension to the Core Rule Set plugin model. Use `rules` for application-specific detections — rules that complement the Core Rule Set rather than pre-empting it. Because they run after the Core Rule Set, they can also read and act on anomaly scores. - -```yaml -rules: | - SecRule REQUEST_BODY "@rx __schema|__type" \ - "id:420020,phase:2,deny,status:403,\ - msg:'GraphQL introspection blocked',\ - logdata:'URI=%{REQUEST_URI}'" -``` - -### `after` — post-Core Rule Set processing - -**Runs last.** Use `after` for false-positive suppression, response inspection, or logging augmentation. Because `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, the cluster operator has the final say on any post-processing — see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#the-merged-ruleset-load-order). - -```yaml -after: | - SecRuleRemoveById 420020 -``` - -:::note - -`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected Core Rule Set baseline rules regardless of slot — the admission webhook will reject the attempt. - -::: - -## Rule IDs and your namespace's range - -Every `SecRule` and `SecAction` must have an ID. Gateway WAF assigns each namespace a contiguous block of 10,000 IDs and enforces it at admission time. Before writing any rule, find your range: - -```bash -kubectl get namespace \ - -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' -# example output: 420000-429999 -``` - -Pick all your rule IDs from inside this range. The annotation is populated the first time any plugin is referenced in your namespace. - -The admission webhook rejects resources with: - -- Rule IDs outside your allocated range (`IDOutsideRange`) -- `SecRuleRemoveById` targeting a protected Core Rule Set baseline rule (`ProtectedIDRemoval`) -- Directives not on the allowlist (`UnsafeDirective`) - -See [SecRule ID ranges](../reference/secrule-id-ranges.mdx) for protected ranges, the derivation algorithm, and the full allowlist. - -## Worked example - -The following `WAFPlugin` uses all four slots to protect a GraphQL API: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: graphql-waf - namespace: platform -spec: - description: "GraphQL introspection guard and false-positive exceptions" - - config: | - # Flag the application type so later rules can branch on it. - SecAction \ - "id:420001,phase:1,pass,nolog,\ - setvar:tx.is_graphql=1" - - before: | - # Allowlist the schema endpoint used by the internal schema registry — - # skip WAF analysis entirely for that specific path. - SecRule REQUEST_URI "@streq /graphql/schema-registry" \ - "id:420010,phase:1,pass,nolog,\ - ctl:ruleEngine=Off,\ - tag:allowlist" - - rules: | - # Block introspection queries (production guard). - SecRule TX:IS_GRAPHQL "@eq 1" \ - "id:420020,phase:2,deny,status:403,\ - msg:'GraphQL introspection blocked',\ - logdata:'body_snippet=%{MATCHED_VAR}',\ - chain" - SecRule REQUEST_BODY "@rx __schema|__type" "" - - # Block suspicious user-agents known to scan GraphQL APIs. - SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ - "id:420021,phase:1,deny,status:403,\ - msg:'Known GraphQL scanner blocked',\ - logdata:'ua=%{MATCHED_VAR}'" - - after: | - # Remove a false positive: CRS rule 941130 fires on a legitimate - # Content-Type value used by our GraphQL client library. - SecRuleRemoveById 941130 -``` - -:::tip - -The `@pmf` operator loads a data file. Data files must be bundled into the Coraza WASM filter image. Ask your cluster operator whether a custom data file is available, or use `@pm` (phrase match inline) for a small list of strings instead. - -::: - -## Attaching the plugin to a WAFPolicy - -A plugin has no effect until a `WAFPolicy` references it. Add the plugin to `spec.plugins[]`; load order is array position. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: platform-api-waf - namespace: platform -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: graphql-api - action: Block - plugins: - - kind: WAFPlugin - name: graphql-waf # ← your plugin above -``` - -If your policy also references a `GlobalWAFPlugin`, the global plugin's slots load before the namespace plugin's slots at each position in the merge order. See [Attach WAF to a route](./attach-waf-to-a-route.mdx) for full `targetRefs` options. - -## Core Rule Set and SecLang references - -Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The Core Rule Set documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. - -:::note - -Not all SecLang directives are permitted. Directives that read from the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](../reference/secrule-id-ranges.mdx#allowed-directives) for the allowlist. - -::: - -## Verify with status conditions - -After applying, check that the plugin was accepted and that the generated `WAFPolicy` reconciled without errors: - -```bash -# Check the plugin was accepted -kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" - -# Check the WAFPolicy reconciled and the filter was programmed -kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" -``` - -A `Ready=True` condition on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed in Envoy Gateway. If the policy is blocked by a validation rule, the status explains which rule fired and why. See [Status conditions](../reference/status-conditions.mdx) for the full condition reference. diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx index 5965e0c175..9750592528 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -1,10 +1,7 @@ --- description: Protect Calico Ingress Gateway traffic with a per-route and per-namespace web application firewall, built on the OWASP Core Rule Set and delivered as a Coraza WebAssembly filter on Envoy Gateway. -hide_table_of_contents: true --- -import { DocCardLink, DocCardLinkLayout } from '/src/___new___/components'; - # Gateway WAF :::note @@ -49,13 +46,11 @@ Gateway WAF splits cleanly across two personas. Cluster-scoped resources belong | App-specific rules and false-positive exceptions | | `WAFPlugin` | | Self-check namespace config (advisory) | | `WAFValidationPolicy` | -Each resource family — Policy, Plugin, and Validation — comes as a cluster-scoped/namespace-scoped pair. See [Policies, plugins, and validation](concepts/policies-plugins-validation.mdx) for when to reach for each. +Each resource family — Policy, Plugin, and Validation — comes as a cluster-scoped/namespace-scoped pair. See [How it works](./how-it-works.mdx) for when to reach for each. ## Next steps - - - - - - +- [How it works](./how-it-works.mdx) — resource families, ownership, merge order, and enforcement. +- [Get started](./get-started.mdx) — enable the feature and block your first attack. +- [Common tasks](./common-tasks.mdx) — set a baseline, attach WAF, write rules, validate config, multi-tenant setup. +- [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx new file mode 100644 index 0000000000..6e119b6738 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -0,0 +1,369 @@ +--- +description: Field reference for the six Gateway WAF custom resources, the Rego validation input schema, SecRule ID range allocation, and status conditions. +--- + +# Reference + +## CRDs + +Gateway WAF defines six custom resources in the `applicationlayer.projectcalico.org/v3` API group. Each family has a cluster-scoped resource and a namespace-scoped resource that share almost all of their fields. + +| Resource | Scope | Short name | Persona | Purpose | +| :--- | :--- | :--- | :--- | :--- | +| `GlobalWAFPolicy` | Cluster | `gwafp` | CO | Cluster-wide baseline (singleton, `name: default`) | +| `WAFPolicy` | Namespace | `wafp` | AO | Attach WAF to a target; per-namespace overrides | +| `GlobalWAFPlugin` | Cluster | `gwafplugin` | CO | Organization-wide custom rules | +| `WAFPlugin` | Namespace | `wafplugin` | AO | App-specific rules and exceptions | +| `GlobalWAFValidationPolicy` | Cluster | `gwafvp` | CO | Compliance guardrails (enforce or audit) | +| `WAFValidationPolicy` | Namespace | `wafvp` | AO | Advisory self-validation | + +### Policy family + +`GlobalWAFPolicy` sets the cluster baseline; `WAFPolicy` attaches WAF to a target and overrides the baseline for its namespace. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default # singleton: name MUST be "default" +spec: + defaultAction: Block # Detect | Block + coreRuleSet: + state: Enabled # Enabled | Disabled + paranoiaLevel: 1 # 1 (balanced) to 4 (paranoid) + plugins: # order = array position + - kind: GlobalWAFPlugin + name: security-baseline +``` + +`WAFPolicy` is namespace-scoped, adds `targetRefs` to attach to a `Gateway` and/or `HTTPRoute`, and its `action` and `coreRuleSet` fields are optional overrides of the global defaults: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: # attach to a Gateway and/or HTTPRoute + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + - group: gateway.networking.k8s.io + kind: Gateway + name: team-gateway + action: Block # optional; nil = inherit global defaultAction + coreRuleSet: + paranoiaLevel: 2 # optional; nil = inherit + plugins: + - kind: WAFPlugin + name: api-exceptions +``` + +**Field reference — Policy family** + +| Field | Type | Resource | Notes | +| :--- | :--- | :--- | :--- | +| `defaultAction` | `Detect` \| `Block` | GlobalWAFPolicy | Cluster default; defaults to `Detect` | +| `action` | `Detect` \| `Block` | WAFPolicy | Optional; **nil = inherit** the global default | +| `coreRuleSet.state` | `Enabled` \| `Disabled` | both | Defaults to `Enabled`; nil on WAFPolicy = inherit | +| `coreRuleSet.paranoiaLevel` | integer 1–4 | both | Defaults to `1`; nil on WAFPolicy = inherit | +| `plugins[]` | list of `{kind, name}` | both | `kind`: `WAFPlugin` \| `GlobalWAFPlugin`; order = array position; max 64 | +| `targetRefs[]` | Gateway API target refs | **WAFPolicy only** | `kind`: `Gateway` \| `HTTPRoute`; min 1, max 16. `sectionName` is reserved for a future release | + +### Plugin family + +`GlobalWAFPlugin` and `WAFPlugin` have identical specs. Each holds custom ModSecurity rules in four lifecycle slots — see [Write custom rules](./common-tasks.mdx#write-custom-rules-with-plugins) for slot semantics. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: security-baseline +spec: + description: "Cluster security baseline" + config: | # 1. set tx.* variables + SecAction "id:1234001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | # 2. pre-CRS processing + SecRule REQUEST_URI "@beginsWith /webhooks/" \ + "id:1234010,phase:1,pass,nolog,ctl:ruleRemoveById=942200" + rules: | # 3. custom rules alongside CRS + SecRule REQUEST_HEADERS:User-Agent "@pmFromFile bad-bots.data" \ + "id:1234020,phase:1,deny,status:403,msg:'Blocked bot'" + after: | # 4. post-CRS processing + SecAction "id:1234030,phase:5,pass,nolog,log" +``` + +`WAFPlugin` uses the identical spec at namespace scope — only `metadata.namespace` and the persona differ. + +**Field reference — Plugin family** + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `description` | string | Human-readable purpose; max 1024 chars | +| `config` | string (SecLang) | Slot 1 — `tx.*` variable setup, runs before Core Rule Set setup; max 65536 chars | +| `before` | string (SecLang) | Slot 2 — pre-Core Rule Set rules; max 65536 chars | +| `rules` | string (SecLang) | Slot 3 — custom rules alongside Core Rule Set (Tigera extension); max 65536 chars | +| `after` | string (SecLang) | Slot 4 — post-Core Rule Set rules; max 65536 chars | + +Rule IDs must fall in your namespace's allocated range — see [SecRule ID ranges](#secrule-id-ranges). + +### Validation family + +`GlobalWAFValidationPolicy` can enforce; `WAFValidationPolicy` is advisory only. Both carry a list of Rego rules — see [Rego validation](#rego-validation) to author the rules. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: security-requirements +spec: + enforcementMode: Enforce # Enforce | Audit (default Audit) + namespaceSelector: # optional; empty = all namespaces + matchLabels: + env: production + rules: + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } +``` + +`WAFValidationPolicy` is namespace-scoped and advisory only — it has no `enforcementMode` and no `namespaceSelector`; every rule is evaluated as if in `Audit` mode. + +**Field reference — Validation family** + +| Field | Type | Resource | Notes | +| :--- | :--- | :--- | :--- | +| `enforcementMode` | `Audit` \| `Enforce` | **GlobalWAFValidationPolicy only** | Defaults to `Audit`. `Enforce` blocks on a `Critical` violation | +| `namespaceSelector` | label selector | **GlobalWAFValidationPolicy only** | Empty = all namespaces | +| `rules[]` | list of `ValidationRule` | both | At least one required | +| `rules[].name` | string | both | 1–253 chars | +| `rules[].rego` | string (Rego module) | both | Must declare `package waf` and a `violations` rule; max 16384 chars | +| `rules[].severity` | `Info` \| `Warning` \| `Critical` | both | Defaults to `Warning` | + +--- + +## Rego validation + +A validation policy carries one or more rules written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), the Open Policy Agent language. Each rule is evaluated **after** the WAF configuration is merged, against the effective result — so a rule can reason about the final action, the final paranoia level, and the complete list of merged directives, regardless of which policy or plugin contributed them. + +### The input document + +Every rule is evaluated against the same `input` document, which describes the merged configuration and its provenance: + +| Path | Type | Meaning | +| :--- | :--- | :--- | +| `input.config.action` | string | Effective action after merge — `Detect` or `Block` | +| `input.config.crsState` | string | Core Rule Set state — `Enabled` or `Disabled` | +| `input.config.paranoiaLevel` | integer | Effective paranoia level, 1–4 | +| `input.directives` | array of string | The complete, ordered list of merged ModSecurity directives | +| `input.source.globalPolicy` | string | Name of the global policy applied (always `default`) | +| `input.source.namespacePolicy` | string | Name of the `WAFPolicy`, if any | +| `input.source.globalPlugins` | array of string | Global plugin names, in load order | +| `input.source.namespacePlugins` | array of string | Namespace plugin names, in load order | + +### The violations contract + +A rule's `rego` field must declare `package waf` and define a `violations` rule that produces a set of objects, each with at least a `msg` field. A namespace with **no** violations is compliant. Any object added to `violations` is a finding; its `msg` is surfaced on the policy status, and any extra fields you include are passed through alongside it. + +```rego +package waf # required — the engine evaluates this package + +# Each block that adds to `violations` is one check. +violations[{"msg": msg}] { # a violation is an object with a msg field + input.config.crsState != "Enabled" # the condition that is NOT allowed + msg := "OWASP CRS must be enabled" +} + +violations[{"msg": msg}] { + input.config.paranoiaLevel < 2 # body lines are ANDed; all must hold + msg := sprintf("paranoia level %d is below the minimum of 2", [input.config.paranoiaLevel]) +} +``` + +You can also inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule: + +```rego +package waf + +violations[{"msg": msg}] { + d := input.directives[_] # iterate the merged directives + startswith(d, "SecRuleRemoveById") + re_match(`94[0-9]{4}`, d) # a CRS SQL-injection rule (942xxx) + msg := sprintf("removes a baseline SQLi rule: %s", [d]) +} +``` + +### Worked examples + +Given the rules above, here is how different merged configurations evaluate: + +| Effective configuration | `violations` | +| :--- | :--- | +| `crsState: Enabled`, `paranoiaLevel: 2` | empty — compliant | +| `crsState: Disabled` | `"OWASP CRS must be enabled"` | +| `paranoiaLevel: 1` | `"paranoia level 1 is below the minimum of 2"` | +| a plugin with `SecRuleRemoveById 942100` | `"removes a baseline SQLi rule: ..."` | + +### Severity + +Each rule has a `severity` of `Info`, `Warning`, or `Critical` (default `Warning`). Severity decides the consequence of a violation, not whether it fires: + +- In a `GlobalWAFValidationPolicy` set to `Enforce`, a `Critical` violation blocks the configuration from being programmed. +- In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. + +See the enforcement table in [How it works](./how-it-works.mdx#validation-and-enforcement) for the full matrix. + +--- + +## SecRule ID ranges + +Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. Gateway WAF gives each namespace a defined range of IDs to use, and stops any namespace from disabling the cluster's baseline rules. Both are enforced through an admission webhook. + +### How a namespace's range is derived + +Each namespace is allocated a contiguous block of 10,000 IDs, derived deterministically from the namespace name: + +```text +prefix = first four hex digits of sha256(namespace name), modulo 10000 +range = [ prefix * 10000 , prefix * 10000 + 9999 ] +``` + +Because the range is a pure function of the namespace name, it never changes and requires no cluster state. This range is the set of IDs your rules may use. It isn't a globally unique reservation: with 10,000 possible blocks, two namespaces can map to the same one, which is harmless. Tenants stay isolated because each namespace's rules run in their own WAF engine, and no namespace can touch the protected ranges below. + +### Finding your range + +On the first time a namespace references a plugin, the controller stamps the range onto the namespace as an annotation: + +```bash +kubectl get namespace payments \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +``` + +Pick the IDs for your `WAFPlugin` rules from inside this range. + +### Protected ranges + +Some IDs are reserved and rejected for every namespace: + +- **`900000`–`999999`** — the OWASP Core Rule Set baseline. +- **Tigera-shipped baseline rules** — the rules that implement the managed security baseline. + +A namespace can neither define rules with these IDs nor remove them with `SecRuleRemoveById`. This is what prevents a tenant from quietly disabling the cluster's protection. + +### What the admission webhook rejects + +When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive and rejects the resource if any directive fails one of these checks. The reason appears on the resource's `Accepted` condition. + +| Check | Rejected with | Meaning | +| :--- | :--- | :--- | +| Directive not on the allowlist | `UnsafeDirective` | The directive type isn't permitted (for example, one that reads files or fetches remote rules) | +| Rule ID outside your range | `IDOutsideRange` | A `SecRule`/`SecAction` ID falls outside the namespace's allocated range | +| Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a Core Rule Set baseline rule) | + +The controller re-runs the same checks during reconciliation as a backstop; if a resource somehow reaches reconcile with a disallowed directive, it is marked `Accepted=False` with the matching reason rather than programmed. + +### Allowed directives + +The allowlist admits the directives needed to write and tune rules — among them `SecRule`, `SecAction`, `SecDefaultAction`, `SecRuleRemoveById`, `SecRuleRemoveByTag`, `SecRuleUpdateActionById`, `SecRuleUpdateTargetById`, `SecMarker`, and the request/response body and argument limits. Directives that read from the filesystem, write audit logs to arbitrary paths, or fetch remote rules are denied. + +### Example + +A namespace whose range is `420000`–`429999` writes rules using IDs from that block: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: api-exceptions + namespace: payments +spec: + rules: | + SecRule REQUEST_BODY "@rx __schema" \ + "id:420020,phase:2,deny,status:403,msg:'Introspection blocked'" +``` + +--- + +## Status conditions + +Gateway WAF reports the state of each policy through standard Kubernetes status conditions. Read them with `kubectl`: + +```bash +kubectl describe wafpolicy payments-waf -n payments +# or +kubectl get wafpolicy payments-waf -n payments -o jsonpath='{.status.conditions}' | jq +``` + +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated roughly in that order — a failure early on explains why later conditions are absent or `False`. + +### Licensed + +Gateway WAF is a $[prodname] feature and requires a valid license. + +| Reason | Meaning | +| :--- | :--- | +| `LicenseValid` | The license entitles this feature | +| `LicenseBlocked` | No valid entitlement; the policy is not programmed | + +### Accepted + +Whether the resource is structurally valid and admitted. Several reasons come straight from the [admission webhook](#what-the-admission-webhook-rejects). + +| Reason | Meaning | +| :--- | :--- | +| `Accepted` | The resource is valid and admitted | +| `Conflicted` | Another policy already targets this route; the older one wins (the message names it) | +| `TargetNotFound` | The referenced `Gateway`/`HTTPRoute` does not exist | +| `UnsupportedTarget` | The `targetRefs` kind is not supported | +| `UnsafeDirective` | A directive is not on the allowlist | +| `IDOutsideRange` | A rule ID is outside the namespace's allocated range | +| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected baseline rule | +| `SectionNameNotYetSupported` | `sectionName` was set; per-rule attachment is reserved for a future release | + +### Validated + +The result of evaluating [validation policies](#rego-validation) against the merged configuration. + +| Reason | Meaning | +| :--- | :--- | +| `Compliant` | No violations | +| `ViolationsFound` | One or more rules reported a violation (see `status.validation.failures`) | +| `NotEvaluated` | No validation policy applies to this resource | + +### Programmed + +Whether the Coraza WASM filter was successfully programmed into Envoy Gateway. + +| Reason | Meaning | +| :--- | :--- | +| `ConfigurationApplied` | The `EnvoyExtensionPolicy` was generated and the filter is active | +| `WASMUnavailable` | The Coraza WASM image could not be pulled | +| `NotAttempted` | A `Critical` violation under `Enforce` blocked programming | + +### Ready + +A roll-up of the conditions above. + +| Reason | Meaning | +| :--- | :--- | +| `Ready` | The policy is active on the data plane | +| `NotReady` | One of the conditions above is not satisfied | +| `LicenseBlocked` | Blocked on licensing | + +### Validation summary + +For convenience, `WAFPolicy.status.validation` summarizes the outcome of validation without having to read individual conditions: + +| Field | Meaning | +| :--- | :--- | +| `status` | `Valid`, `Audited`, or `Rejected` | +| `securityPosture` | `Compliant`, `Warning`, `Degraded`, or `Critical` | +| `failures[]` | Per-rule failures — policy name, rule, severity, and message | +| `lastEvaluated` | When validation last ran | + +A `GlobalWAFValidationPolicy`'s own status aggregates the violating namespaces cluster-wide, giving the cluster operator a single place to see which namespaces are out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/reference/crds.mdx b/calico-enterprise/threat/gateway-waf/reference/crds.mdx deleted file mode 100644 index e51e95bffb..0000000000 --- a/calico-enterprise/threat/gateway-waf/reference/crds.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -description: Field reference for the six Gateway WAF custom resources, grouped by family with cluster-scoped and namespace-scoped examples. ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# CRD reference - -Gateway WAF defines six custom resources in the `applicationlayer.projectcalico.org/v3` API group. Each family has a cluster-scoped resource and a namespace-scoped resource that share almost all of their fields. - -| Resource | Scope | Short name | Persona | Purpose | -| :--- | :--- | :--- | :--- | :--- | -| `GlobalWAFPolicy` | Cluster | `gwafp` | CO | Cluster-wide baseline (singleton, `name: default`) | -| `WAFPolicy` | Namespace | `wafp` | AO | Attach WAF to a target; per-namespace overrides | -| `GlobalWAFPlugin` | Cluster | `gwafplugin` | CO | Organization-wide custom rules | -| `WAFPlugin` | Namespace | `wafplugin` | AO | App-specific rules and exceptions | -| `GlobalWAFValidationPolicy` | Cluster | `gwafvp` | CO | Compliance guardrails (enforce or audit) | -| `WAFValidationPolicy` | Namespace | `wafvp` | AO | Advisory self-validation | - -## Policy family - -`GlobalWAFPolicy` sets the cluster baseline; `WAFPolicy` attaches WAF to a target and overrides the baseline for its namespace. - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default # singleton: name MUST be "default" -spec: - defaultAction: Block # Detect | Block - coreRuleSet: - state: Enabled # Enabled | Disabled - paranoiaLevel: 1 # 1 (balanced) to 4 (paranoid) - plugins: # order = array position - - kind: GlobalWAFPlugin - name: security-baseline -``` - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-waf - namespace: payments -spec: - targetRefs: # attach to a Gateway and/or HTTPRoute - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - - group: gateway.networking.k8s.io - kind: Gateway - name: team-gateway - action: Block # optional; nil = inherit global defaultAction - coreRuleSet: - paranoiaLevel: 2 # optional; nil = inherit - plugins: - - kind: WAFPlugin - name: api-exceptions -``` - - - - -
-Field reference — Policy family - -| Field | Type | Resource | Notes | -| :--- | :--- | :--- | :--- | -| `defaultAction` | `Detect` \| `Block` | GlobalWAFPolicy | Cluster default; defaults to `Detect` | -| `action` | `Detect` \| `Block` | WAFPolicy | Optional; **nil = inherit** the global default | -| `coreRuleSet.state` | `Enabled` \| `Disabled` | both | Defaults to `Enabled`; nil on WAFPolicy = inherit | -| `coreRuleSet.paranoiaLevel` | integer 1–4 | both | Defaults to `1`; nil on WAFPolicy = inherit | -| `plugins[]` | list of `{kind, name}` | both | `kind`: `WAFPlugin` \| `GlobalWAFPlugin`; order = array position; max 64 | -| `targetRefs[]` | Gateway API target refs | **WAFPolicy only** | `kind`: `Gateway` \| `HTTPRoute`; min 1, max 16. `sectionName` is reserved for a future release | - -
- -## Plugin family - -`GlobalWAFPlugin` and `WAFPlugin` have identical specs. Each holds custom ModSecurity rules in four lifecycle slots. See [Write custom rules](../how-to/write-custom-rules.mdx) for slot semantics. - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPlugin -metadata: - name: security-baseline -spec: - description: "Cluster security baseline" - config: | # 1. set tx.* variables - SecAction "id:1234001,phase:1,pass,nolog,setvar:tx.enforce_https=1" - before: | # 2. pre-CRS processing - SecRule REQUEST_URI "@beginsWith /webhooks/" \ - "id:1234010,phase:1,pass,nolog,ctl:ruleRemoveById=942200" - rules: | # 3. custom rules alongside CRS - SecRule REQUEST_HEADERS:User-Agent "@pmFromFile bad-bots.data" \ - "id:1234020,phase:1,deny,status:403,msg:'Blocked bot'" - after: | # 4. post-CRS processing - SecAction "id:1234030,phase:5,pass,nolog,log" -``` - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: api-exceptions - namespace: payments -spec: - description: "GraphQL introspection guard" - config: | - SecAction "id:5678001,phase:1,pass,nolog,setvar:tx.graphql_max_depth=10" - rules: | - SecRule REQUEST_BODY "@rx __schema" \ - "id:5678020,phase:2,deny,status:403,msg:'Introspection blocked'" -``` - - - - -
-Field reference — Plugin family - -| Field | Type | Notes | -| :--- | :--- | :--- | -| `description` | string | Human-readable purpose; max 1024 chars | -| `config` | string (SecLang) | Slot 1 — `tx.*` variable setup, runs before Core Rule Set setup; max 65536 chars | -| `before` | string (SecLang) | Slot 2 — pre-Core Rule Set rules; max 65536 chars | -| `rules` | string (SecLang) | Slot 3 — custom rules alongside Core Rule Set (Tigera extension); max 65536 chars | -| `after` | string (SecLang) | Slot 4 — post-Core Rule Set rules; max 65536 chars | - -Rule IDs must fall in your namespace's allocated range — see [SecRule ID ranges](secrule-id-ranges.mdx). - -
- -## Validation family - -`GlobalWAFValidationPolicy` can enforce; `WAFValidationPolicy` is advisory only. Both carry a list of Rego rules. See [Rego validation](rego-validation.mdx) to author the rules. - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: security-requirements -spec: - enforcementMode: Enforce # Enforce | Audit (default Audit) - namespaceSelector: # optional; empty = all namespaces - matchLabels: - env: production - rules: - - name: crs-must-be-enabled - severity: Critical - rego: | - package waf - violations[{"msg": "OWASP CRS must be enabled"}] { - input.config.crsState != "Enabled" - } -``` - - - - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFValidationPolicy -metadata: - name: team-self-check - namespace: payments -spec: - # no enforcementMode and no namespaceSelector — advisory only - rules: - - name: paranoia-minimum - severity: Warning - rego: | - package waf - violations[{"msg": sprintf("paranoia %d below 2", [input.config.paranoiaLevel])}] { - input.config.paranoiaLevel < 2 - } -``` - - - - -
-Field reference — Validation family - -| Field | Type | Resource | Notes | -| :--- | :--- | :--- | :--- | -| `enforcementMode` | `Audit` \| `Enforce` | **GlobalWAFValidationPolicy only** | Defaults to `Audit`. `Enforce` blocks on a `Critical` violation | -| `namespaceSelector` | label selector | **GlobalWAFValidationPolicy only** | Empty = all namespaces | -| `rules[]` | list of `ValidationRule` | both | At least one required | -| `rules[].name` | string | both | 1–253 chars | -| `rules[].rego` | string (Rego module) | both | Must declare `package waf` and a `violations` rule; max 16384 chars | -| `rules[].severity` | `Info` \| `Warning` \| `Critical` | both | Defaults to `Warning` | - -
diff --git a/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx b/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx deleted file mode 100644 index 46971c78b1..0000000000 --- a/calico-enterprise/threat/gateway-waf/reference/rego-validation.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -description: Author Rego validation rules for Gateway WAF — the input document schema, the violations contract, and worked pass/fail examples. ---- - -# Rego validation - -A validation policy carries one or more rules written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), the Open Policy Agent language. Each rule is evaluated **after** the WAF configuration is merged, against the effective result — so a rule can reason about the final action, the final paranoia level, and the complete list of merged directives, regardless of which policy or plugin contributed them. - -This page is the authoring contract. To attach rules to a cluster or namespace, see [Validate configuration](../how-to/validate-config.mdx); for how a violation affects reconciliation, see [Merge order and enforcement](../concepts/merge-and-enforcement.mdx#validation-and-enforcement). - -## The input document - -Every rule is evaluated against the same `input` document, which describes the merged configuration and its provenance: - -| Path | Type | Meaning | -| :--- | :--- | :--- | -| `input.config.action` | string | Effective action after merge — `Detect` or `Block` | -| `input.config.crsState` | string | Core Rule Set state — `Enabled` or `Disabled` | -| `input.config.paranoiaLevel` | integer | Effective paranoia level, 1–4 | -| `input.directives` | array of string | The complete, ordered list of merged ModSecurity directives | -| `input.source.globalPolicy` | string | Name of the global policy applied (always `default`) | -| `input.source.namespacePolicy` | string | Name of the `WAFPolicy`, if any | -| `input.source.globalPlugins` | array of string | Global plugin names, in load order | -| `input.source.namespacePlugins` | array of string | Namespace plugin names, in load order | - -## The violations contract - -A rule's `rego` field must: - -1. Declare `package waf`. -2. Define a `violations` rule that produces a set of objects, each with at least a `msg` field. - -A namespace with **no** violations is compliant. Any object added to `violations` is a finding; its `msg` is surfaced on the policy status, and any extra fields you include are passed through alongside it. - -```rego -package waf # required — the engine evaluates this package - -# Each block that adds to `violations` is one check. -violations[{"msg": msg}] { # a violation is an object with a msg field - input.config.crsState != "Enabled" # the condition that is NOT allowed - msg := "OWASP CRS must be enabled" -} - -violations[{"msg": msg}] { - input.config.paranoiaLevel < 2 # body lines are ANDed; all must hold - msg := sprintf("paranoia level %d is below the minimum of 2", [input.config.paranoiaLevel]) -} -``` - -You can also inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule: - -```rego -package waf - -violations[{"msg": msg}] { - d := input.directives[_] # iterate the merged directives - startswith(d, "SecRuleRemoveById") - re_match(`94[0-9]{4}`, d) # a CRS SQL-injection rule (942xxx) - msg := sprintf("removes a baseline SQLi rule: %s", [d]) -} -``` - -## Worked examples - -Given the rules above, here is how different merged configurations evaluate: - -| Effective configuration | `violations` | -| :--- | :--- | -| `crsState: Enabled`, `paranoiaLevel: 2` | empty — compliant | -| `crsState: Disabled` | `"OWASP CRS must be enabled"` | -| `paranoiaLevel: 1` | `"paranoia level 1 is below the minimum of 2"` | -| a plugin with `SecRuleRemoveById 942100` | `"removes a baseline SQLi rule: ..."` | - -## Severity - -Each rule has a `severity` of `Info`, `Warning`, or `Critical` (default `Warning`). Severity decides the consequence of a violation, not whether it fires: - -- In a `GlobalWAFValidationPolicy` set to `Enforce`, a `Critical` violation blocks the configuration from being programmed. -- In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. - -See the [enforcement table](../concepts/merge-and-enforcement.mdx#validation-and-enforcement) for the full matrix. diff --git a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx b/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx deleted file mode 100644 index ace3c0af28..0000000000 --- a/calico-enterprise/threat/gateway-waf/reference/secrule-id-ranges.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -description: How Gateway WAF assigns each namespace a SecRule ID range, which rule IDs are protected, and what the admission webhook rejects. ---- - -# SecRule ID ranges - -Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. Gateway WAF gives each namespace a defined range of IDs to use, and stops any namespace from disabling the cluster's baseline rules. Both are enforced through an admission webhook. - -## How a namespace's range is derived - -Each namespace is allocated a contiguous block of 10,000 IDs, derived deterministically from the namespace name: - -```text -prefix = first four hex digits of sha256(namespace name), modulo 10000 -range = [ prefix * 10000 , prefix * 10000 + 9999 ] -``` - -Because the range is a pure function of the namespace name, it never changes and requires no cluster state. This range is the set of IDs your rules may use. It isn't a globally unique reservation: with 10,000 possible blocks, two namespaces can map to the same one, which is harmless. Tenants stay isolated because each namespace's rules run in their own WAF engine, and no namespace can touch the protected ranges below. - -## Finding your range - -On the first time a namespace references a plugin, the controller stamps the range onto the namespace as an annotation: - -```bash -kubectl get namespace payments \ - -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' -``` - -Pick the IDs for your `WAFPlugin` rules from inside this range. - -## Protected ranges - -Some IDs are reserved and rejected for every namespace: - -- **`900000`–`999999`** — the OWASP Core Rule Set baseline. -- **Tigera-shipped baseline rules** — the rules that implement the managed security baseline. - -A namespace can neither define rules with these IDs nor remove them with `SecRuleRemoveById`. This is what prevents a tenant from quietly disabling the cluster's protection. - -## What the admission webhook rejects - -When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive and rejects the resource if any directive fails one of these checks. The reason appears on the resource's `Accepted` condition. - -| Check | Rejected with | Meaning | -| :--- | :--- | :--- | -| Directive not on the allowlist | `UnsafeDirective` | The directive type isn't permitted (for example, one that reads files or fetches remote rules) | -| Rule ID outside your range | `IDOutsideRange` | A `SecRule`/`SecAction` ID falls outside the namespace's allocated range | -| Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a Core Rule Set baseline rule) | - -The controller re-runs the same checks during reconciliation as a backstop; if a resource somehow reaches reconcile with a disallowed directive, it is marked `Accepted=False` with the matching reason rather than programmed. - -## Allowed directives - -The allowlist admits the directives needed to write and tune rules — among them `SecRule`, `SecAction`, `SecDefaultAction`, `SecRuleRemoveById`, `SecRuleRemoveByTag`, `SecRuleUpdateActionById`, `SecRuleUpdateTargetById`, `SecMarker`, and the request/response body and argument limits. Directives that read from the filesystem, write audit logs to arbitrary paths, or fetch remote rules are denied. - -## Example - -A namespace whose range is `420000`–`429999` writes rules using IDs from that block: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: api-exceptions - namespace: payments -spec: - rules: | - SecRule REQUEST_BODY "@rx __schema" \ - "id:420020,phase:2,deny,status:403,msg:'Introspection blocked'" -``` diff --git a/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx b/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx deleted file mode 100644 index 673d287966..0000000000 --- a/calico-enterprise/threat/gateway-waf/reference/status-conditions.mdx +++ /dev/null @@ -1,82 +0,0 @@ ---- -description: Read the status conditions Gateway WAF sets on its resources — Licensed, Accepted, Programmed, Validated, and Ready — and what each reason means. ---- - -# Status conditions - -Gateway WAF reports the state of each policy through standard Kubernetes status conditions. Read them with `kubectl`: - -```bash -kubectl describe wafpolicy payments-waf -n payments -# or -kubectl get wafpolicy payments-waf -n payments -o jsonpath='{.status.conditions}' | jq -``` - -A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated roughly in that order — a failure early on explains why later conditions are absent or `False`. - -## Licensed - -Gateway WAF is a $[prodname] feature and requires a valid license. - -| Reason | Meaning | -| :--- | :--- | -| `LicenseValid` | The license entitles this feature | -| `LicenseBlocked` | No valid entitlement; the policy is not programmed | - -## Accepted - -Whether the resource is structurally valid and admitted. Several reasons come straight from the [admission webhook](secrule-id-ranges.mdx#what-the-admission-webhook-rejects). - -| Reason | Meaning | -| :--- | :--- | -| `Accepted` | The resource is valid and admitted | -| `Conflicted` | Another policy already targets this route; the older one wins (the message names it) | -| `TargetNotFound` | The referenced `Gateway`/`HTTPRoute` does not exist | -| `UnsupportedTarget` | The `targetRefs` kind is not supported | -| `UnsafeDirective` | A directive is not on the allowlist | -| `IDOutsideRange` | A rule ID is outside the namespace's allocated range | -| `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected baseline rule | -| `SectionNameNotYetSupported` | `sectionName` was set; per-rule attachment is reserved for a future release | - -## Validated - -The result of evaluating [validation policies](rego-validation.mdx) against the merged configuration. - -| Reason | Meaning | -| :--- | :--- | -| `Compliant` | No violations | -| `ViolationsFound` | One or more rules reported a violation (see `status.validation.failures`) | -| `NotEvaluated` | No validation policy applies to this resource | - -## Programmed - -Whether the Coraza WASM filter was successfully programmed into Envoy Gateway. - -| Reason | Meaning | -| :--- | :--- | -| `ConfigurationApplied` | The `EnvoyExtensionPolicy` was generated and the filter is active | -| `WASMUnavailable` | The Coraza WASM image could not be pulled | -| `NotAttempted` | A `Critical` violation under `Enforce` blocked programming | - -## Ready - -A roll-up of the conditions above. - -| Reason | Meaning | -| :--- | :--- | -| `Ready` | The policy is active on the data plane | -| `NotReady` | One of the conditions above is not satisfied | -| `LicenseBlocked` | Blocked on licensing | - -## Validation summary - -For convenience, `WAFPolicy.status.validation` summarizes the outcome of validation without having to read individual conditions: - -| Field | Meaning | -| :--- | :--- | -| `status` | `Valid`, `Audited`, or `Rejected` | -| `securityPosture` | `Compliant`, `Warning`, `Degraded`, or `Critical` | -| `failures[]` | Per-rule failures — policy name, rule, severity, and message | -| `lastEvaluated` | When validation last ran | - -A `GlobalWAFValidationPolicy`'s own status aggregates the violating namespaces cluster-wide, giving the cluster operator a single place to see which namespaces are out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index f04bdb6a47..c923b3c858 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -10,7 +10,7 @@ Most Gateway WAF problems surface directly on the policy's status conditions. St kubectl describe wafpolicy -n ``` -A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated in roughly that order — a failure early on explains why later conditions are absent or `False`. For the full meaning of each condition and reason, see [Status conditions](./reference/status-conditions.mdx). +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated in roughly that order — a failure early on explains why later conditions are absent or `False`. For the full meaning of each condition and reason, see [Status conditions](./reference.mdx#status-conditions). --- @@ -24,7 +24,7 @@ A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Progra kubectl get wafpolicy -n -o wide ``` -Either delete the conflicting policy, or change one policy's `targetRefs` so they no longer point at the same target. To understand why only one policy can own a target, see [Merge order and enforcement](./concepts/merge-and-enforcement.mdx). +Either delete the conflicting policy, or change one policy's `targetRefs` so they no longer point at the same target. To understand why only one policy can own a target, see [How it works](./how-it-works.mdx#merge-order-and-enforcement). --- @@ -52,7 +52,7 @@ kubectl get namespace \ -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' ``` -For `UnsafeDirective`: remove or replace the disallowed directive. See [SecRule ID ranges](./reference/secrule-id-ranges.mdx) for the full allowlist and protected range details. +For `UnsafeDirective`: remove or replace the disallowed directive. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the full allowlist and protected range details. --- @@ -94,7 +94,7 @@ kubectl get wafpolicy -n \ The `failures[]` array shows which `GlobalWAFValidationPolicy` rule failed, with its severity and message. The `status.validation.status` field will be `Rejected`. -**Fix.** Correct the `WAFPolicy` to satisfy the failing rule — for example, change `action: Detect` back to `action: Block` if the validation policy requires Block mode. If you believe the policy should be an exception, ask the cluster operator to adjust the `GlobalWAFValidationPolicy`. For how to read and act on validation output, see [Validate config](./how-to/validate-config.mdx) and [Status conditions](./reference/status-conditions.mdx). +**Fix.** Correct the `WAFPolicy` to satisfy the failing rule — for example, change `action: Detect` back to `action: Block` if the validation policy requires Block mode. If you believe the policy should be an exception, ask the cluster operator to adjust the `GlobalWAFValidationPolicy`. For how to read and act on validation output, see [Validate configuration with Rego](./common-tasks.mdx#validate-configuration-with-rego) and [Status conditions](./reference.mdx#status-conditions). --- @@ -140,7 +140,7 @@ Also verify the `GlobalWAFPolicy` default action — a namespace `WAFPolicy` tha kubectl get globalwafpolicy default -o jsonpath='{.spec.defaultAction}' ``` -See [Global vs. namespaced](./concepts/global-vs-namespaced.mdx) for field inheritance rules. +See [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart) for field inheritance rules. --- @@ -163,7 +163,7 @@ The log lines will include the rule ID and message that triggered the block. 1. **Lower the paranoia level** if the false positives come from a large class of higher-paranoia rules and your threat model allows it. 2. **Add a plugin exception** to suppress the specific rule ID using `SecRuleRemoveById` or to skip the rule for a known-safe target using `SecRuleUpdateTargetById`. This is the preferred approach for surgical tuning without weakening the overall posture. -For how to write exception rules within your namespace's ID range, see [Write custom rules](./how-to/write-custom-rules.mdx). +For how to write exception rules within your namespace's ID range, see [Write custom rules with plugins](./common-tasks.mdx#write-custom-rules-with-plugins). :::caution @@ -182,20 +182,8 @@ kubectl get wafpolicy -n \ -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}): {.message}{"\n"}{end}' ``` -1. `Licensed=False` → The license is missing or does not entitle Gateway WAF. Check [Enable Gateway WAF](./get-started/enable.mdx). +1. `Licensed=False` → The license is missing or does not entitle Gateway WAF. Check [Get started](./get-started.mdx). 2. `Accepted=False` → The policy spec is rejected. Read the `Reason` and `message` — see the sections above. 3. `Validated=False` → A validation rule fired. Read `status.validation.failures`. 4. `Programmed=False` → The filter was not loaded into Envoy. Read the `Reason` — see `WASMUnavailable` and `NotAttempted` above. 5. All conditions `True` but WAF not blocking → Allow a few seconds for Envoy to load the WASM module after `EnvoyExtensionPolicy` creation. If it still does not block, the effective action is likely `Detect` — see [Attacks not blocked](#attacks-not-blocked) above. - -## Related pages - -- [Reference: Status conditions](./reference/status-conditions.mdx) -- [Reference: SecRule ID ranges](./reference/secrule-id-ranges.mdx) -- [Reference: Rego validation](./reference/rego-validation.mdx) -- [Reference: CRDs](./reference/crds.mdx) -- [Concepts: Merge order and enforcement](./concepts/merge-and-enforcement.mdx) -- [Concepts: Global vs. namespaced](./concepts/global-vs-namespaced.mdx) -- [How-to: Attach WAF to a route](./how-to/attach-waf-to-a-route.mdx) -- [How-to: Write custom rules](./how-to/write-custom-rules.mdx) -- [How-to: Validate config](./how-to/validate-config.mdx) diff --git a/sidebars-calico-enterprise.js b/sidebars-calico-enterprise.js index 7ac4f663fd..f599789080 100644 --- a/sidebars-calico-enterprise.js +++ b/sidebars-calico-enterprise.js @@ -477,44 +477,10 @@ module.exports = { label: 'Gateway WAF', link: { type: 'doc', id: 'threat/gateway-waf/index' }, items: [ - { - type: 'category', - label: 'Concepts', - items: [ - 'threat/gateway-waf/concepts/policies-plugins-validation', - 'threat/gateway-waf/concepts/global-vs-namespaced', - 'threat/gateway-waf/concepts/merge-and-enforcement', - ], - }, - { - type: 'category', - label: 'Get started', - items: [ - 'threat/gateway-waf/get-started/enable', - 'threat/gateway-waf/get-started/quickstart', - ], - }, - { - type: 'category', - label: 'How to', - items: [ - 'threat/gateway-waf/how-to/set-a-baseline', - 'threat/gateway-waf/how-to/attach-waf-to-a-route', - 'threat/gateway-waf/how-to/write-custom-rules', - 'threat/gateway-waf/how-to/validate-config', - 'threat/gateway-waf/how-to/multi-tenant-setup', - ], - }, - { - type: 'category', - label: 'Reference', - items: [ - 'threat/gateway-waf/reference/crds', - 'threat/gateway-waf/reference/rego-validation', - 'threat/gateway-waf/reference/secrule-id-ranges', - 'threat/gateway-waf/reference/status-conditions', - ], - }, + 'threat/gateway-waf/how-it-works', + 'threat/gateway-waf/get-started', + 'threat/gateway-waf/common-tasks', + 'threat/gateway-waf/reference', 'threat/gateway-waf/troubleshooting', ], }, From de0351c0326eb0a21bbc7e44c4118e4bab1ee1e6 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 15:26:37 +0100 Subject: [PATCH 09/16] docs(gateway-waf): trim prose toward the preview budget Verbosity-only pass over Common tasks, Reference, How it works, and Troubleshooting: cut repeated narrative and duplicate YAML, keep one representative example per task. No facts, tables, field/rule/status-condition names, or meaning changed. ~10.0k -> ~8.5k words. Ref: EV-6466 --- .../threat/gateway-waf/common-tasks.mdx | 171 +++++++----------- .../threat/gateway-waf/how-it-works.mdx | 64 +++---- .../threat/gateway-waf/reference.mdx | 48 ++--- .../threat/gateway-waf/troubleshooting.mdx | 66 +++---- 4 files changed, 147 insertions(+), 202 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/common-tasks.mdx b/calico-enterprise/threat/gateway-waf/common-tasks.mdx index 38132fe71b..a4c1c0171a 100644 --- a/calico-enterprise/threat/gateway-waf/common-tasks.mdx +++ b/calico-enterprise/threat/gateway-waf/common-tasks.mdx @@ -8,19 +8,15 @@ description: Set a cluster-wide WAF baseline, attach WAF to a Gateway or route, :::note[Cluster operator task] -This task is for cluster operators who own the `GlobalWAFPolicy` singleton and set the security posture that all namespaces inherit. +Owns the `GlobalWAFPolicy` singleton and sets the baseline every namespace inherits. ::: -Before any application operator can attach WAF to a route, you need a cluster baseline. The baseline defines the default action, Core Rule Set state, paranoia level, and any organization-wide custom rules. Application operators inherit these settings; they can tighten them, but a [validation policy](#validate-configuration-with-rego) can prevent them from weakening them. - -**Prerequisites:** the Gateway WAF feature gate is enabled on the `ApplicationLayer` resource (see [Get started](./get-started.mdx)), and you have cluster-admin rights — the `GlobalWAFPolicy` API is cluster-scoped. +**Prerequisites:** the Gateway WAF feature gate is enabled on `ApplicationLayer` (see [Get started](./get-started.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. ### Create the GlobalWAFPolicy singleton -`GlobalWAFPolicy` must be named `default`. Applying any other name is rejected by the admission webhook. - -Start in `Detect` mode. `Detect` logs matched requests but allows them through, giving you time to measure false positives before you switch to `Block`. +Must be named `default` — other names are rejected by the admission webhook. Start in `Detect` mode: it logs matches without blocking, so you can measure false positives before switching to `Block`. ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -34,9 +30,9 @@ spec: paranoiaLevel: 1 ``` -Apply it with `kubectl apply -f globalwafpolicy.yaml`. +Apply with `kubectl apply -f globalwafpolicy.yaml`. -The OWASP Core Rule Set defines four paranoia levels. Each higher level adds more detection rules and catches more attacks, at the cost of more false positives: +Four paranoia levels; higher catches more attacks at the cost of more false positives: | Level | Posture | Typical use | | :--- | :--- | :--- | @@ -47,15 +43,15 @@ The OWASP Core Rule Set defines four paranoia levels. Each higher level adds mor :::tip -Start at paranoia level 1 cluster-wide. Let application operators raise the level for their own namespaces if their traffic demands it. You can enforce a minimum level later with a validation policy. +Start at level 1 cluster-wide; let application operators raise it per namespace, and enforce a minimum later via a validation policy. ::: -Once you've confirmed the false-positive rate is acceptable, change `defaultAction` to `Block`. In `Block` mode, $[prodname] returns HTTP 403 for any request that matches a WAF rule. +Once satisfied, set `defaultAction: Block` — $[prodname] then returns HTTP 403 for matching requests. ### Add organization-wide rules with a GlobalWAFPlugin -A `GlobalWAFPlugin` holds custom ModSecurity rules that apply to every namespace through the global policy's `plugins[]` list. Create the plugin first, then reference it from `GlobalWAFPolicy`. Rules are organized into the four lifecycle slots described in [Write custom rules](#write-custom-rules-with-plugins). +A `GlobalWAFPlugin` holds custom ModSecurity rules applied cluster-wide via the global policy's `plugins[]` list. Create it, then reference it — rules use the four lifecycle slots in [Write custom rules](#write-custom-rules-with-plugins). ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -79,16 +75,7 @@ spec: "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" ``` -Reference it from `GlobalWAFPolicy.spec.plugins[]`: - -```yaml -spec: - plugins: - - kind: GlobalWAFPlugin - name: org-baseline -``` - -`plugins[]` is an ordered list; the merger processes plugins in array position order. Add more `GlobalWAFPlugin` entries as your organization grows — up to 64 total. You can add `WAFPlugin` entries to `GlobalWAFPolicy.spec.plugins[]` as well. +Reference it from `GlobalWAFPolicy.spec.plugins[]` (`plugins: [{kind: GlobalWAFPlugin, name: org-baseline}]`). `plugins[]` is ordered by array position, up to 64 entries; `WAFPlugin` entries can be added here too. ### Verify the baseline @@ -101,9 +88,9 @@ NAME ACCEPTED AGE default True 30s ``` -For detailed status, inspect `.status.conditions` with `kubectl get globalwafpolicy default -o yaml`. An `Accepted=True` condition with reason `Reconciled` means the baseline is programmed. If a validation policy blocked the configuration, you will see `Accepted=False` with a `ValidationFailed` reason and a message describing the violation. See [Status conditions](./reference.mdx#status-conditions) for the full condition reference. +Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Reconciled` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). -Every namespace that has a `WAFPolicy` attached to a target inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy`. A `WAFPolicy` can override any of these fields for its namespace; unset fields fall back to the global value: +Every namespace with a `WAFPolicy` inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy` unless it overrides them: ``` GlobalWAFPolicy (baseline) @@ -117,13 +104,13 @@ GlobalWAFPolicy (baseline) :::note[Application operator task] -This task is for application operators who own namespace-scoped WAF resources and decide which of their Gateways and routes receive WAF inspection. +Owns namespace-scoped WAF resources and decides which of their Gateways and routes get WAF inspection. ::: -WAF inspection does not start automatically when the cluster operator enables the feature. You activate it by creating a `WAFPolicy` in your namespace that points to one or more Gateway API targets. The policy inherits the cluster baseline and lets you layer namespace-specific overrides on top. +Create a `WAFPolicy` in your namespace pointing at Gateway API targets — WAF inspection doesn't start automatically when the feature is enabled. It inherits the cluster baseline with namespace overrides layered on top. -**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy` (see above), you have write access to `WAFPolicy` resources in your namespace, and a `Gateway` or `HTTPRoute` in your namespace is already receiving traffic. +**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy`; you have write access to `WAFPolicy` in your namespace; a `Gateway`/`HTTPRoute` is already receiving traffic. `WAFPolicy.spec.targetRefs[]` accepts two kinds of targets: @@ -132,7 +119,7 @@ WAF inspection does not start automatically when the cluster operator enables th | `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | | `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | -Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` on Envoy Gateway. A policy with two entries generates two filters — one per target. Up to 16 entries are allowed in `targetRefs`. +Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` (max 16 entries). :::note @@ -140,7 +127,7 @@ Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` on Envoy Gat ::: -This example attaches WAF to a single `HTTPRoute` and overrides the action and paranoia level for this namespace. Unset fields (here, `coreRuleSet.state`) still inherit from the cluster baseline. +Attaching WAF to one `HTTPRoute`, overriding action and paranoia; the unset `coreRuleSet.state` still inherits from the baseline: ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -159,17 +146,11 @@ spec: # coreRuleSet.state is unset → inherited from GlobalWAFPolicy ``` -Apply it with `kubectl apply -f wafpolicy.yaml`. - -To add rules beyond the cluster baseline, create a `WAFPlugin` and reference it from `WAFPolicy.spec.plugins[]` — see [Write custom rules](#write-custom-rules-with-plugins). - -For how `action` and `coreRuleSet` fields inherit from `GlobalWAFPolicy`, see [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart). +Apply with `kubectl apply -f wafpolicy.yaml`. For custom rules beyond the baseline, see [Write custom rules](#write-custom-rules-with-plugins); for inheritance details, see [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart). ### Conflict resolution -Only one `WAFPolicy` may be the active policy for any given target at a time. If two `WAFPolicy` resources in the same namespace both target the same Gateway or HTTPRoute, $[prodname] follows the [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) tie-breaking rule: the policy with the oldest `creationTimestamp` wins, and the newer policy is marked `Accepted=False` with reason `Conflicted`. Only the winning policy's directives are merged and programmed. - -To resolve a conflict, delete the newer policy or update its `targetRefs` to point at a different target. +Only one `WAFPolicy` may be active per target. If two in the same namespace target the same Gateway/HTTPRoute, $[prodname] follows [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins; the newer is marked `Accepted=False` reason `Conflicted`, and only the winner's directives are merged. Fix by deleting the newer policy or repointing its `targetRefs`. ### Verify the policy @@ -182,12 +163,10 @@ NAME ACCEPTED PROGRAMMED AGE payments-api-waf True True 45s ``` -The `.status.conditions` field reports `Accepted`, `Programmed`, and any validation findings — see [Status conditions](./reference.mdx#status-conditions). To confirm Envoy Gateway received the filter, run `kubectl get envoyextensionpolicies -n payments`. Each `targetRefs` entry produces one `EnvoyExtensionPolicy`. +`.status.conditions` reports `Accepted`, `Programmed`, and validation findings. Confirm Envoy Gateway received the filter with `kubectl get envoyextensionpolicies -n payments` (one per `targetRefs` entry). ### Test the WAF -Send a request that includes a classic SQL injection pattern. In `Block` mode this should return HTTP 403: - ```bash curl -si "https:///api?id=1+OR+1%3D1" ``` @@ -202,7 +181,7 @@ Access denied :::tip -If the Gateway is running in `Detect` mode (the cluster default until you upgrade to `Block`), the request will be allowed through but the WAF event will appear in the Envoy Gateway logs. Check with: +In `Detect` mode the request is allowed but the WAF event appears in the Envoy Gateway logs: ```bash kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "waf\|coraza\|modsec" @@ -212,7 +191,7 @@ kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "w :::caution -If you get `ACCEPTED=False` with reason `ValidationFailed`, a `GlobalWAFValidationPolicy` is blocking your configuration. The status message identifies which policy and rule triggered the violation. Contact your cluster operator or see [Validate configuration with Rego](#validate-configuration-with-rego) for the self-check workflow. +`ACCEPTED=False` reason `ValidationFailed` means a `GlobalWAFValidationPolicy` is blocking your config — the status message names the policy and rule. Contact your cluster operator or see [Validate configuration with Rego](#validate-configuration-with-rego). ::: @@ -222,32 +201,34 @@ If you get `ACCEPTED=False` with reason `ValidationFailed`, a `GlobalWAFValidati :::note[Application operator task] -This task is for application operators who manage WAF configuration within a namespace. Cluster operators who want organization-wide rules create a `GlobalWAFPlugin` instead and reference it from `GlobalWAFPolicy.spec.plugins[]` — the CRD fields are identical; only scope differs. +Cluster operators wanting org-wide rules use `GlobalWAFPlugin` instead, referenced from `GlobalWAFPolicy.spec.plugins[]` — same fields, different scope. ::: -A `WAFPlugin` (namespace-scoped) or `GlobalWAFPlugin` (cluster-scoped) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives organized into four lifecycle slots, modeled on the Core Rule Set 4 plugin file structure. Understanding the load order (see [How it works](./how-it-works.mdx#the-merged-ruleset-load-order)) is the key to knowing which slot to use: +A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives in four lifecycle slots, modeled on the Core Rule Set 4 plugin structure. Slot choice follows [load order](./how-it-works.mdx#the-merged-ruleset-load-order): -- **`config`** — variable setup. Runs before Core Rule Set setup. Use it to set `tx.*` transaction variables the Core Rule Set reads during its own initialization: tuning anomaly score thresholds, enabling optional Core Rule Set features, or setting application-specific flags. If a rule fires on a request variable (such as a request header), it belongs in `before` instead — at `config` time the request has not been read yet. -- **`before`** — pre-Core Rule Set rules. Runs after variable setup but before the Core Rule Set loads. Use it for early-exit allowlisting of trusted paths, request normalization, or setting flags that later rules can read. -- **`rules`** — custom rules alongside Core Rule Set. Runs after the full Core Rule Set has executed. This is a Tigera extension to the Core Rule Set plugin model. Use it for application-specific detections that complement the Core Rule Set rather than pre-empting it; because these run after the Core Rule Set, they can also read and act on anomaly scores. -- **`after`** — post-Core Rule Set processing. Runs last. Use it for false-positive suppression, response inspection, or logging augmentation. Because `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, the cluster operator has the final say on any post-processing. +| Slot | Runs | Use for | +| :--- | :--- | :--- | +| `config` | Before Core Rule Set setup | `tx.*` variable setup (anomaly thresholds, optional features, app flags). Rules on request variables belong in `before` instead — the request isn't read yet | +| `before` | After variable setup, before Core Rule Set loads | Early-exit allowlisting of trusted paths, request normalization, flags for later rules | +| `rules` | Alongside Core Rule Set, after it (Tigera extension) | App-specific detections that complement rather than pre-empt the Core Rule Set; can act on anomaly scores | +| `after` | Last | False-positive suppression, response inspection, logging. `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, so the cluster operator has final say | :::caution -`ctl:ruleEngine=Off` inside a chained rule or as a per-request action disables the engine for that transaction only. Disabling it globally via a top-level `SecRuleEngine Off` directive is not permitted and will be rejected by the admission webhook. +`ctl:ruleEngine=Off` inside a chained rule or per-request disables the engine for that transaction only. A top-level `SecRuleEngine Off` is rejected by the admission webhook. ::: :::note -`SecRuleRemoveById` and `ctl:ruleRemoveById` in the `after` slot apply for the remainder of phase processing. Use them to suppress false positives your own rules produce. You cannot remove protected Core Rule Set baseline rules regardless of slot — the admission webhook will reject the attempt. +`SecRuleRemoveById`/`ctl:ruleRemoveById` in `after` apply for the rest of phase processing — use to suppress your own false positives. Protected Core Rule Set baseline rules can't be removed from any slot; the webhook rejects the attempt. ::: ### Rule IDs and your namespace's range -Every `SecRule` and `SecAction` must have an ID. Gateway WAF assigns each namespace a contiguous block of 10,000 IDs and enforces it at admission time. Find your range before writing any rule: +Every `SecRule`/`SecAction` needs an ID. Each namespace gets a 10,000-ID block, enforced at admission. Find your range first: ```bash kubectl get namespace \ @@ -255,11 +236,11 @@ kubectl get namespace \ # example output: 420000-429999 ``` -Pick all your rule IDs from inside this range. The annotation is populated the first time any plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the full directive allowlist. +Populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. ### Worked example -The following `WAFPlugin` uses all four slots to protect a GraphQL API: +This `WAFPlugin` uses all four slots to protect a GraphQL API: ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -307,11 +288,11 @@ spec: :::tip -The `@pmf` operator loads a data file. Data files must be bundled into the Coraza WASM filter image. Ask your cluster operator whether a custom data file is available, or use `@pm` (phrase match inline) for a small list of strings instead. +`@pmf` loads a data file, which must be bundled into the Coraza WASM filter image — ask your cluster operator, or use `@pm` (inline phrase match) for a short string list. ::: -A plugin has no effect until a `WAFPolicy` references it. Add it to `spec.plugins[]`; load order is array position: +A plugin has no effect until a `WAFPolicy` references it, in `spec.plugins[]` (load order = array position): ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -330,42 +311,42 @@ spec: name: graphql-waf # ← your plugin above ``` -If your policy also references a `GlobalWAFPlugin`, the global plugin's slots load before the namespace plugin's slots at each position in the merge order. +If the policy also references a `GlobalWAFPlugin`, its slots load before the namespace plugin's slots at each position. -Gateway WAF runs the [OWASP Core Rule Set v4](https://coreruleset.org/docs/) via the [Coraza](https://coraza.io/) engine. The Core Rule Set documentation covers available `tx.*` variables, rule tag conventions, and anomaly scoring. The [ModSecurity reference manual](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) covers the full SecLang directive set. +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. :::note -Not all SecLang directives are permitted. Directives that read from the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. +Not all SecLang directives are permitted — directives that read the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. ::: -After applying, check that the plugin was accepted and that the generated `WAFPolicy` reconciled without errors: +Confirm acceptance and reconciliation: ```bash kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" ``` -A `Ready=True` condition on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed in Envoy Gateway. +`Ready=True` on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed. --- ## Validate configuration with Rego -Gateway WAF has two guardrail layers. The admission webhook checks raw SecLang at apply time (structural safety). Validation policies check the **merged result** at reconcile time — after all policies and plugins have been combined — so they can catch problems that are only visible in the assembled directive list. For the full enforcement matrix, see [How it works](./how-it-works.mdx#validation-and-enforcement); for the input schema and authoring contract, see [Rego validation](./reference.mdx#rego-validation). +Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang at apply time (structural safety); validation policies check the **merged result** at reconcile time, catching problems only visible once everything's merged. See [Validation and enforcement](./how-it-works.mdx#validation-and-enforcement) for the matrix and [Rego validation](./reference.mdx#rego-validation) for the input schema. ### Cluster operator: GlobalWAFValidationPolicy :::note[Cluster operator] -`GlobalWAFValidationPolicy` is a cluster-scoped resource. Only cluster operators can create or modify it. It applies to every namespace, or to a label-selected subset via `namespaceSelector`. +Cluster-scoped; applies to every namespace or a label-selected subset via `namespaceSelector`. ::: -A `GlobalWAFValidationPolicy` carries a list of Rego rules and an `enforcementMode`: `Audit` (default) records violations on status while the WAF filter is still programmed; `Enforce` blocks the filter from being programmed on a `Critical` violation. `Warning` and `Info` violations are recorded on status regardless of mode. +Carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. -The following policy enforces three requirements for all namespaces labeled `env: production`, and audits a fourth: +This policy enforces three requirements for namespaces labeled `env: production`, and audits a fourth: ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -425,26 +406,17 @@ spec: :::tip -Start every new `GlobalWAFValidationPolicy` in `Audit` mode. Review the violations that appear on existing `WAFPolicy` status before switching to `Enforce`. This avoids blocking teams whose configurations predate the new policy. +Start new policies in `Audit` mode; review violations on existing `WAFPolicy` status before switching to `Enforce`, so you don't block teams whose config predates the policy. ::: -Leave `namespaceSelector` empty (or omit it) to apply the policy to every namespace, or scope it to a subset: - -```yaml -spec: - namespaceSelector: - matchLabels: - compliance: pci-dss # only namespaces with this label -``` - -Multiple `GlobalWAFValidationPolicy` objects can coexist. Every matching policy's rules are evaluated; a single `Critical` violation from any of them (when in `Enforce` mode) is enough to block programming. +Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. ### Application operator: WAFValidationPolicy :::note[Application operator] -`WAFValidationPolicy` is namespace-scoped. It is advisory only — violations appear on status but never block the WAF filter from being programmed. Use it as a self-check for your namespace's configuration. +Namespace-scoped and advisory only — violations appear on status but never block programming. Use as a self-check. ::: @@ -466,13 +438,11 @@ spec: } ``` -Because `WAFValidationPolicy` is advisory, it is safe to define your own standards here that are stricter than the cluster baseline — for example, requiring a minimum paranoia level higher than the global default. Violations surface on the `WAFPolicy` status and prompt the author to fix the config, but they do not interrupt traffic. +Being advisory, it's safe to hold yourself to a stricter standard than the baseline — e.g. a higher minimum paranoia level. Violations surface on `WAFPolicy` status without interrupting traffic. ### Reading violation results -Violations from all matching validation policies appear in the status conditions of the affected `WAFPolicy`. Check them with `kubectl get wafpolicy -n -o yaml`. - -A blocked policy looks like: +Violations from matching policies appear in the affected `WAFPolicy`'s status conditions. Check with `kubectl get wafpolicy -n -o yaml`. A blocked policy: ```yaml status: @@ -487,26 +457,11 @@ status: status: "True" ``` -An audit violation (filter still programmed) looks like: - -```yaml -status: - conditions: - - type: Ready - status: "True" - - type: ValidationWarning - status: "True" - reason: AuditViolation - message: > - GlobalWAFValidationPolicy/production-requirements rule "rce-rules-intact" [Warning]: - RCE rule removal detected in merged config: SecRuleRemoveById 932100 -``` - -See [Status conditions](./reference.mdx#status-conditions) for the complete condition type reference. +An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. :::tip -Write your Rego rules against the input schema in [Rego validation](./reference.mdx#rego-validation) and test them locally with `opa eval` before applying to the cluster. A syntax error in a rule causes the entire policy to be marked invalid; the status reports which rule failed to parse. +Write Rego against the schema in [Rego validation](./reference.mdx#rego-validation); test locally with `opa eval` first. A syntax error invalidates the whole policy — status reports which rule failed to parse. ::: @@ -516,13 +471,13 @@ Write your Rego rules against the input schema in [Rego validation](./reference. :::note[Scenario] -A single cluster shared by two application teams — `team-a` and `payments` — and a cluster operator who sets the security baseline. This walks through the full CO ↔ AO collaboration: a `GlobalWAFPolicy` baseline, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that prevents application teams from weakening protection. Each team then attaches WAF to their own Gateway and tunes it independently. +A cluster shared by `team-a` and `payments`, with a cluster operator setting the baseline via a `GlobalWAFPolicy`, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that stops teams weakening protection. Each team attaches WAF to its own Gateway and tunes independently. -**Prerequisites:** Gateway WAF is enabled and a valid license is in place. +**Prerequisites:** Gateway WAF enabled; valid license. ::: -The cluster operator creates the `default` `GlobalWAFPolicy`, the `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` in `Enforce` mode scoped to namespaces labeled `env: production` — following the patterns above. Application operators need to read `Global*` resources to understand what baseline they are inheriting. Grant read access with a `ClusterRole` and a per-namespace `RoleBinding`: +The cluster operator creates the `default` `GlobalWAFPolicy`, `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` (`Enforce`, scoped to `env: production`) — per the patterns above. Application operators need read access to `Global*` resources to see what they're inheriting: ```yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -554,26 +509,26 @@ roleRef: :::note -This grants read on cluster-scoped resources via a namespace-scoped `RoleBinding`. Application operators can see the baseline and validation policy, but cannot create or edit `Global*` resources. +Grants read on cluster-scoped resources via a namespace-scoped `RoleBinding` — application operators can see but not edit `Global*` resources. ::: -Each team labels its namespace (`kubectl label namespace team-a env=production`, and the same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. The `team-a` operator raises paranoia to 2 and inherits `coreRuleSet.state: Enabled` from the baseline. The `payments` operator runs stricter still: paranoia 3, plus a namespace `WAFPlugin` that blocks GraphQL introspection using an ID from its own allocated [SecRule ID range](./reference.mdx#secrule-id-ranges). +Each team labels its namespace (`kubectl label namespace team-a env=production`, same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. `team-a` raises paranoia to 2, inheriting `coreRuleSet.state: Enabled`; `payments` runs paranoia 3 plus a namespace `WAFPlugin` blocking GraphQL introspection, using an ID from its own [SecRule ID range](./reference.mdx#secrule-id-ranges). ### What happens if a team tries to weaken the baseline -If the `payments` operator applies a policy with `action: Detect`, the `production-requirements` `GlobalWAFValidationPolicy` (in `Enforce` mode) fires the `must-use-block-mode` Critical rule. The controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation in `status.validation.failures`: +If `payments` applies `action: Detect`, `production-requirements` (`Enforce`) fires its `must-use-block-mode` Critical rule — the controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation: ```bash kubectl get wafpolicy payments-waf -n payments \ -o jsonpath='{.status.validation.failures}' | jq ``` -No `EnvoyExtensionPolicy` is generated until the policy is corrected. The operator must restore `action: Block` or request a policy exception from the cluster operator. +No `EnvoyExtensionPolicy` is generated until corrected — restore `action: Block` or request an exception from the cluster operator. ### Result: isolation in practice -- **`team-a`** runs Core Rule Set at paranoia 2 with its own custom rules, scoped to its ID range. -- **`payments`** runs Core Rule Set at paranoia 3 plus its GraphQL introspection block, scoped to a different ID range. -- Neither team can disable the org-wide rule from `GlobalWAFPlugin` or lower below Block mode. -- The cluster operator sees all validation violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — a single place to audit which namespaces are out of compliance. +- **`team-a`** — Core Rule Set at paranoia 2, own custom rules, own ID range. +- **`payments`** — Core Rule Set at paranoia 3, GraphQL introspection block, a different ID range. +- Neither team can disable the org-wide rule from `GlobalWAFPlugin` or drop below Block mode. +- The cluster operator sees all violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — one place to see which namespaces are non-compliant. diff --git a/calico-enterprise/threat/gateway-waf/how-it-works.mdx b/calico-enterprise/threat/gateway-waf/how-it-works.mdx index 3d59841fe4..adaca3c7e0 100644 --- a/calico-enterprise/threat/gateway-waf/how-it-works.mdx +++ b/calico-enterprise/threat/gateway-waf/how-it-works.mdx @@ -4,7 +4,7 @@ description: How Gateway WAF resources are organized, how cluster-scoped and nam # How it works -Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group, arranged as three families. Each family has a cluster-scoped resource, owned by the **cluster operator (CO)**, and a namespace-scoped resource, owned by the **application operator (AO)**. +Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group, in three families. Each has a cluster-scoped resource owned by the **cluster operator (CO)** and a namespace-scoped resource owned by the **application operator (AO)**. | Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | | :--- | :--- | :--- | :--- | @@ -12,50 +12,40 @@ Gateway WAF is configured through six custom resources in the `applicationlayer. | **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | | **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | -The cluster operator *sets* the security posture; the application operator *tunes* it for their workloads. An AO can make their namespace stricter, and can be prevented from making it weaker through validation policies (see [Validation and enforcement](#validation-and-enforcement)). +The cluster operator *sets* the posture; the application operator *tunes* it — can go stricter, but can be stopped from going weaker (see [Validation and enforcement](#validation-and-enforcement)). ## Policies — what to enforce -A **policy** declares the high-level WAF configuration: the action mode (`Detect` or `Block`), whether the OWASP Core Rule Set is enabled, the paranoia level, and which plugins to load. +A **policy** declares the action mode (`Detect`/`Block`), whether the Core Rule Set is enabled, the paranoia level, and which plugins to load. -- `GlobalWAFPolicy` is a cluster-wide singleton (`name: default`) that sets the baseline for every namespace. -- `WAFPolicy` is namespace-scoped. It attaches WAF to a Gateway or `HTTPRoute` through `targetRefs`, and can override the baseline action and paranoia for that namespace. - -Reach for a policy when you want to turn WAF on for some traffic and set its overall posture. +- `GlobalWAFPolicy` — cluster-wide singleton (`name: default`), sets the baseline for every namespace. +- `WAFPolicy` — namespace-scoped, attaches WAF to a Gateway/`HTTPRoute` via `targetRefs`, can override the baseline action and paranoia. ## Plugins — custom rules -A **plugin** is a container for custom ModSecurity rules, organized into four lifecycle slots — `config`, `before`, `rules`, and `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Plugins are referenced from a policy's `spec.plugins[]` list; they do nothing on their own. - -- `GlobalWAFPlugin` holds organization-wide rules — security baselines, compliance rules. -- `WAFPlugin` holds application-specific rules and false-positive exceptions. +A **plugin** holds custom ModSecurity rules in four lifecycle slots — `config`, `before`, `rules`, `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Referenced from a policy's `spec.plugins[]`; does nothing on its own. -Reach for a plugin when the Core Rule Set isn't enough — you need a custom detection rule, a pre-processing step, or an exception for a known false positive. +- `GlobalWAFPlugin` — organization-wide rules: security baselines, compliance rules. +- `WAFPlugin` — application-specific rules and false-positive exceptions. ## Validation policies — guardrails -A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules that are evaluated against the *merged, effective* WAF configuration. They answer "is this configuration acceptable?" — for example, "the Core Rule Set must stay enabled" or "production namespaces must run in Block mode." +A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated against the *merged, effective* configuration — answering "is this acceptable?", e.g. "Core Rule Set must stay enabled" or "production must run in Block mode." -- `GlobalWAFValidationPolicy` is enforced by the cluster operator. In `Enforce` mode a `Critical` violation blocks the configuration from being programmed; in `Audit` mode it only warns. A `namespaceSelector` scopes which namespaces it applies to. -- `WAFValidationPolicy` is advisory only — it surfaces violations on the policy status for the application operator to act on, but never blocks. There is no enforce mode at namespace scope. +- `GlobalWAFValidationPolicy` — enforced by the cluster operator: `Enforce` mode blocks programming on a `Critical` violation, `Audit` mode only warns. `namespaceSelector` scopes which namespaces it applies to. +- `WAFValidationPolicy` — advisory only, surfaces violations on status for the application operator to act on, never blocks. No enforce mode at namespace scope. -Reach for a validation policy when you want to codify a requirement rather than a rule. - -:::note - -Validation policies are not the same as the admission webhook. The webhook checks the *raw SecLang* you submit (safe directives, allowed rule IDs) at write time; validation policies check the *merged result* for compliance. See [Validation and enforcement](#validation-and-enforcement) below. - -::: +(Validation policies aren't the admission webhook — see [Validation and enforcement](#validation-and-enforcement) for the distinction.) ## The global policy is a singleton -`GlobalWAFPolicy` is a cluster-wide singleton: it must be named `default`. It establishes the cluster baseline — default action, Core Rule Set state, and paranoia level — that every namespace inherits unless it overrides. +`GlobalWAFPolicy` is a cluster-wide singleton — must be named `default`. It sets the baseline (default action, Core Rule Set state, paranoia level) every namespace inherits unless it overrides. -`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons; you can create as many as you need. +`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons — create as many as needed. ## Inheritance: namespaces override, they don't restart -A `WAFPolicy` does not have to restate the whole baseline. Its `action` and `coreRuleSet` fields are optional — when unset, the namespace inherits the value from `GlobalWAFPolicy`. +A `WAFPolicy` doesn't have to restate the whole baseline — its `action` and `coreRuleSet` fields are optional; unset fields inherit from `GlobalWAFPolicy`. ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -82,19 +72,19 @@ spec: ## Plugins are additive, not overriding -Plugins don't override each other — they stack. When a target is configured, the merger applies **all** referenced global plugins first (in `GlobalWAFPolicy.spec.plugins[]` order), then all referenced namespace plugins (in `WAFPolicy.spec.plugins[]` order). Order is array position; there is no priority field. +Plugins stack rather than override. The merger applies all referenced global plugins first (`GlobalWAFPolicy.spec.plugins[]` order), then all namespace plugins (`WAFPolicy.spec.plugins[]` order) — order is array position; there's no priority field. -This is what lets a cluster operator's baseline rules and an application operator's exceptions coexist in a single ruleset. The exact interleaving — and why the global `after` slot runs last — is covered below. +This lets baseline rules and application exceptions coexist in one ruleset — the interleaving, and why global `after` runs last, is covered below. ## Merge order and enforcement -When a `WAFPolicy` attaches to a target, the controller gathers every policy and plugin that applies, merges them into a single ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each entry in `targetRefs[]` produces its own `EnvoyExtensionPolicy`, so a policy attached to three routes generates three filters. +When a `WAFPolicy` attaches to a target, the controller gathers every applicable policy and plugin, merges them into one ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each `targetRefs[]` entry produces its own `EnvoyExtensionPolicy` — three routes means three filters. -Three things govern the outcome: the **merge order** (how the ruleset is assembled), the **admission webhook** (what raw SecLang you're allowed to submit), and **validation policies** (whether the merged result is compliant). +Three things govern the outcome: **merge order**, the **admission webhook**, and **validation policies**. ### The merged ruleset load order -Directive order is not cosmetic — the Core Rule Set requires configuration variables to be set before the ruleset loads, and post-processing to run after. The merger assembles directives in this fixed order: +Directive order isn't cosmetic: the Core Rule Set needs variables set before loading, and post-processing after. Fixed order: **Engine setup** 1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. @@ -124,17 +114,17 @@ Directive order is not cosmetic — the Core Rule Set requires configuration var 13. Namespace plugin `after` rules. 14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. -The pattern is global-before-namespace through the early slots, and namespace-before-global in the `after` slot. Within each plugin, its own slots always run in `config` → `before` → `rules` → `after` order. +Pattern: global-before-namespace in the early slots, namespace-before-global in `after`. Within a plugin, its own slots always run `config` → `before` → `rules` → `after`. :::note -When two `WAFPolicy` resources target the same route, $[prodname] follows the Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/) default: the policy with the oldest `creationTimestamp` wins (ties broken by `namespace/name`). The loser is marked `Accepted=False` with reason `Conflicted`, and its directives are not merged. +When two `WAFPolicy` resources target the same route, $[prodname] follows Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins (ties by `namespace/name`). The loser is marked `Accepted=False` reason `Conflicted`, directives not merged. ::: ### Structural safety: the admission webhook -Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at the moment you apply it. It parses each directive and rejects the resource if any directive fails one of three checks: +Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at apply time, rejecting the resource if any directive fails one of three checks: | Check | Passes when | Rejected with | | :--- | :--- | :--- | @@ -142,13 +132,13 @@ Before any of the above runs, a validating admission webhook checks the raw SecL | Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | | Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | -This is what stops one namespace from colliding with the rule IDs of another, or disabling the cluster's baseline Core Rule Set rules. Each namespace is automatically assigned a 10,000-ID range; see [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for how the range is derived and which rule IDs are protected. +This stops one namespace colliding with another's rule IDs, or disabling the cluster's baseline Core Rule Set rules. Each namespace gets an automatic 10,000-ID range; see [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for how it's derived and which IDs are protected. ### Validation and enforcement -The admission webhook checks *raw SecLang*. Validation policies check the *merged result* — and they run after the merge precisely because some problems are only visible there. Two innocent plugins can combine to disable all SQL-injection rules; no admission-time check can see that, but a Rego rule evaluated against the merged directive list can. +The admission webhook checks *raw SecLang*; validation policies check the *merged result*, running after the merge because some problems only show up there. Two innocent plugins can combine to disable all SQL-injection rules — invisible to admission-time checks, but visible to a Rego rule against the merged directive list. -Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with a severity of `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: +Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with severity `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: | Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | | :--- | :--- | :--- | :--- | @@ -156,4 +146,4 @@ Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-l | `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | | `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | -To write the rules themselves, see [Rego validation](./reference.mdx#rego-validation). To read the outcome, see [Status conditions](./reference.mdx#status-conditions). +To write the rules, see [Rego validation](./reference.mdx#rego-validation). To read the outcome, see [Status conditions](./reference.mdx#status-conditions). diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx index 6e119b6738..4828253985 100644 --- a/calico-enterprise/threat/gateway-waf/reference.mdx +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -6,7 +6,7 @@ description: Field reference for the six Gateway WAF custom resources, the Rego ## CRDs -Gateway WAF defines six custom resources in the `applicationlayer.projectcalico.org/v3` API group. Each family has a cluster-scoped resource and a namespace-scoped resource that share almost all of their fields. +Six custom resources in the `applicationlayer.projectcalico.org/v3` API group, in three families; each has a cluster-scoped and namespace-scoped resource sharing almost all fields. | Resource | Scope | Short name | Persona | Purpose | | :--- | :--- | :--- | :--- | :--- | @@ -36,7 +36,7 @@ spec: name: security-baseline ``` -`WAFPolicy` is namespace-scoped, adds `targetRefs` to attach to a `Gateway` and/or `HTTPRoute`, and its `action` and `coreRuleSet` fields are optional overrides of the global defaults: +`WAFPolicy` adds `targetRefs` (`Gateway`/`HTTPRoute`); `action` and `coreRuleSet` are optional overrides of the global defaults: ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -73,7 +73,7 @@ spec: ### Plugin family -`GlobalWAFPlugin` and `WAFPlugin` have identical specs. Each holds custom ModSecurity rules in four lifecycle slots — see [Write custom rules](./common-tasks.mdx#write-custom-rules-with-plugins) for slot semantics. +`GlobalWAFPlugin`/`WAFPlugin` share an identical spec: custom ModSecurity rules in four lifecycle slots — see [Write custom rules](./common-tasks.mdx#write-custom-rules-with-plugins). ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -110,7 +110,7 @@ Rule IDs must fall in your namespace's allocated range — see [SecRule ID range ### Validation family -`GlobalWAFValidationPolicy` can enforce; `WAFValidationPolicy` is advisory only. Both carry a list of Rego rules — see [Rego validation](#rego-validation) to author the rules. +`GlobalWAFValidationPolicy` can enforce; `WAFValidationPolicy` is advisory only. Both carry Rego rules — see [Rego validation](#rego-validation). ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -132,7 +132,7 @@ spec: } ``` -`WAFValidationPolicy` is namespace-scoped and advisory only — it has no `enforcementMode` and no `namespaceSelector`; every rule is evaluated as if in `Audit` mode. +`WAFValidationPolicy` is namespace-scoped, advisory only — no `enforcementMode`, no `namespaceSelector`; every rule runs as if in `Audit` mode. **Field reference — Validation family** @@ -149,11 +149,11 @@ spec: ## Rego validation -A validation policy carries one or more rules written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), the Open Policy Agent language. Each rule is evaluated **after** the WAF configuration is merged, against the effective result — so a rule can reason about the final action, the final paranoia level, and the complete list of merged directives, regardless of which policy or plugin contributed them. +A validation policy carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated after the configuration is merged — so a rule can reason about the final action, paranoia level, and merged directive list, regardless of which policy or plugin contributed them. ### The input document -Every rule is evaluated against the same `input` document, which describes the merged configuration and its provenance: +Every rule evaluates against the same `input` document — the merged configuration and its provenance: | Path | Type | Meaning | | :--- | :--- | :--- | @@ -168,7 +168,7 @@ Every rule is evaluated against the same `input` document, which describes the m ### The violations contract -A rule's `rego` field must declare `package waf` and define a `violations` rule that produces a set of objects, each with at least a `msg` field. A namespace with **no** violations is compliant. Any object added to `violations` is a finding; its `msg` is surfaced on the policy status, and any extra fields you include are passed through alongside it. +A rule's `rego` field must declare `package waf` and a `violations` rule producing objects with at least a `msg` field. No violations means compliant; each object is a finding, surfaced on status, with any extra fields passed through. ```rego package waf # required — the engine evaluates this package @@ -185,7 +185,7 @@ violations[{"msg": msg}] { } ``` -You can also inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule: +Or inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule: ```rego package waf @@ -200,7 +200,7 @@ violations[{"msg": msg}] { ### Worked examples -Given the rules above, here is how different merged configurations evaluate: +How different merged configurations evaluate against the rules above: | Effective configuration | `violations` | | :--- | :--- | @@ -211,9 +211,9 @@ Given the rules above, here is how different merged configurations evaluate: ### Severity -Each rule has a `severity` of `Info`, `Warning`, or `Critical` (default `Warning`). Severity decides the consequence of a violation, not whether it fires: +Each rule has a `severity` — `Info`, `Warning`, `Critical` (default `Warning`) — deciding the consequence, not whether it fires: -- In a `GlobalWAFValidationPolicy` set to `Enforce`, a `Critical` violation blocks the configuration from being programmed. +- In `Enforce` mode, a `Critical` violation blocks the configuration from being programmed. - In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. See the enforcement table in [How it works](./how-it-works.mdx#validation-and-enforcement) for the full matrix. @@ -222,7 +222,7 @@ See the enforcement table in [How it works](./how-it-works.mdx#validation-and-en ## SecRule ID ranges -Every custom rule you write in a `WAFPlugin` carries a numeric ModSecurity ID. Gateway WAF gives each namespace a defined range of IDs to use, and stops any namespace from disabling the cluster's baseline rules. Both are enforced through an admission webhook. +Every custom rule in a `WAFPlugin` carries a numeric ModSecurity ID. Gateway WAF allocates each namespace a fixed ID range and blocks any namespace from disabling baseline rules — both enforced by the admission webhook. ### How a namespace's range is derived @@ -233,11 +233,11 @@ prefix = first four hex digits of sha256(namespace name), modulo 10000 range = [ prefix * 10000 , prefix * 10000 + 9999 ] ``` -Because the range is a pure function of the namespace name, it never changes and requires no cluster state. This range is the set of IDs your rules may use. It isn't a globally unique reservation: with 10,000 possible blocks, two namespaces can map to the same one, which is harmless. Tenants stay isolated because each namespace's rules run in their own WAF engine, and no namespace can touch the protected ranges below. +The range is a pure function of the namespace name — it never changes and needs no cluster state. It isn't a globally unique reservation: two namespaces can map to the same block, harmlessly, since each runs its own WAF engine and neither can touch the protected ranges below. ### Finding your range -On the first time a namespace references a plugin, the controller stamps the range onto the namespace as an annotation: +The first time a namespace references a plugin, the controller stamps the range onto it as an annotation: ```bash kubectl get namespace payments \ @@ -253,11 +253,11 @@ Some IDs are reserved and rejected for every namespace: - **`900000`–`999999`** — the OWASP Core Rule Set baseline. - **Tigera-shipped baseline rules** — the rules that implement the managed security baseline. -A namespace can neither define rules with these IDs nor remove them with `SecRuleRemoveById`. This is what prevents a tenant from quietly disabling the cluster's protection. +A namespace can neither define rules with these IDs nor remove them with `SecRuleRemoveById` — stopping a tenant from quietly disabling cluster protection. ### What the admission webhook rejects -When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive and rejects the resource if any directive fails one of these checks. The reason appears on the resource's `Accepted` condition. +Applying a `WAFPlugin`/`WAFPolicy` triggers the webhook to parse every directive; failing any check below rejects the resource, with the reason on its `Accepted` condition. | Check | Rejected with | Meaning | | :--- | :--- | :--- | @@ -265,11 +265,11 @@ When you apply a `WAFPlugin` or `WAFPolicy`, the webhook parses every directive | Rule ID outside your range | `IDOutsideRange` | A `SecRule`/`SecAction` ID falls outside the namespace's allocated range | | Removing a protected rule | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets an ID outside the namespace's range (such as a Core Rule Set baseline rule) | -The controller re-runs the same checks during reconciliation as a backstop; if a resource somehow reaches reconcile with a disallowed directive, it is marked `Accepted=False` with the matching reason rather than programmed. +The controller re-runs these checks at reconcile as a backstop, marking `Accepted=False` with the matching reason instead of programming. ### Allowed directives -The allowlist admits the directives needed to write and tune rules — among them `SecRule`, `SecAction`, `SecDefaultAction`, `SecRuleRemoveById`, `SecRuleRemoveByTag`, `SecRuleUpdateActionById`, `SecRuleUpdateTargetById`, `SecMarker`, and the request/response body and argument limits. Directives that read from the filesystem, write audit logs to arbitrary paths, or fetch remote rules are denied. +Allowed: `SecRule`, `SecAction`, `SecDefaultAction`, `SecRuleRemoveById`, `SecRuleRemoveByTag`, `SecRuleUpdateActionById`, `SecRuleUpdateTargetById`, `SecMarker`, and request/response body and argument limits. Denied: directives that read the filesystem, write audit logs to arbitrary paths, or fetch remote rules. ### Example @@ -291,7 +291,7 @@ spec: ## Status conditions -Gateway WAF reports the state of each policy through standard Kubernetes status conditions. Read them with `kubectl`: +Gateway WAF reports policy state through standard Kubernetes status conditions: ```bash kubectl describe wafpolicy payments-waf -n payments @@ -299,7 +299,7 @@ kubectl describe wafpolicy payments-waf -n payments kubectl get wafpolicy payments-waf -n payments -o jsonpath='{.status.conditions}' | jq ``` -A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated roughly in that order — a failure early on explains why later conditions are absent or `False`. +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, `Ready=True`, evaluated roughly in that order — an early failure explains why later conditions are absent or `False`. ### Licensed @@ -337,7 +337,7 @@ The result of evaluating [validation policies](#rego-validation) against the mer ### Programmed -Whether the Coraza WASM filter was successfully programmed into Envoy Gateway. +Whether the Coraza WASM filter was programmed into Envoy Gateway. | Reason | Meaning | | :--- | :--- | @@ -357,7 +357,7 @@ A roll-up of the conditions above. ### Validation summary -For convenience, `WAFPolicy.status.validation` summarizes the outcome of validation without having to read individual conditions: +`WAFPolicy.status.validation` summarizes validation without reading individual conditions: | Field | Meaning | | :--- | :--- | @@ -366,4 +366,4 @@ For convenience, `WAFPolicy.status.validation` summarizes the outcome of validat | `failures[]` | Per-rule failures — policy name, rule, severity, and message | | `lastEvaluated` | When validation last ran | -A `GlobalWAFValidationPolicy`'s own status aggregates the violating namespaces cluster-wide, giving the cluster operator a single place to see which namespaces are out of compliance. +A `GlobalWAFValidationPolicy`'s own status aggregates violating namespaces cluster-wide — one place to see who's out of compliance. diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index c923b3c858..eb2432f4dc 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -4,33 +4,33 @@ description: Diagnose and fix common Gateway WAF problems by reading policy stat # Troubleshooting -Most Gateway WAF problems surface directly on the policy's status conditions. Start here: +Most problems surface on the policy's status conditions: ```bash kubectl describe wafpolicy -n ``` -A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, and `Ready=True`. The conditions are evaluated in roughly that order — a failure early on explains why later conditions are absent or `False`. For the full meaning of each condition and reason, see [Status conditions](./reference.mdx#status-conditions). +A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, `Ready=True` — evaluated roughly in order, so an early failure explains later absences. See [Status conditions](./reference.mdx#status-conditions) for reason meanings. --- ## `Accepted=False, Reason=Conflicted` -**Cause.** Two `WAFPolicy` resources in the same namespace target the same Gateway or `HTTPRoute`. $[prodname] follows Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): the policy with the oldest `creationTimestamp` wins. The newer policy is marked `Conflicted` and its directives are never merged. +**Cause.** Two `WAFPolicy` resources in the same namespace target the same Gateway/HTTPRoute. Per [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/), oldest `creationTimestamp` wins; the newer is marked `Conflicted`, directives never merged. -**Fix.** Identify which policies are competing: +**Fix.** Identify the competing policies: ```bash kubectl get wafpolicy -n -o wide ``` -Either delete the conflicting policy, or change one policy's `targetRefs` so they no longer point at the same target. To understand why only one policy can own a target, see [How it works](./how-it-works.mdx#merge-order-and-enforcement). +Delete one, or repoint its `targetRefs` — see [How it works](./how-it-works.mdx#merge-order-and-enforcement) for why only one policy owns a target. --- ## `Accepted=False, Reason=UnsafeDirective | IDOutsideRange | ProtectedIDRemoval` -**Cause.** The admission webhook rejected the SecLang in a `WAFPlugin` or `WAFPolicy` at apply time. These checks happen before the resource reaches the controller. +**Cause.** The admission webhook rejected the SecLang in a `WAFPlugin` or `WAFPolicy` at apply time, before the resource reached the controller. | Reason | Meaning | | :--- | :--- | @@ -45,22 +45,22 @@ kubectl get wafpolicy -n \ -o jsonpath='{.status.conditions[?(@.type=="Accepted")].message}' ``` -For `IDOutsideRange` and `ProtectedIDRemoval`: look up your namespace's allocated ID range and use only IDs from that block: +For `IDOutsideRange`/`ProtectedIDRemoval`: use only IDs from your namespace's range: ```bash kubectl get namespace \ -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' ``` -For `UnsafeDirective`: remove or replace the disallowed directive. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the full allowlist and protected range details. +For `UnsafeDirective`: remove or replace the directive. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. --- ## `Programmed=False, Reason=WASMUnavailable` -**Cause.** The Coraza WASM image could not be pulled by the Envoy proxy. This is typically a registry access problem: the image reference is unreachable, or a private registry requires a pull secret that has not been configured. +**Cause.** The Envoy proxy couldn't pull the Coraza WASM image — usually a registry issue: an unreachable reference, or a missing pull secret. -**Diagnostic.** Check the `EnvoyExtensionPolicy` status that the controller generated for the policy: +**Diagnostic.** Check the `EnvoyExtensionPolicy` status the controller generated: ```bash kubectl get envoyextensionpolicy \ @@ -69,7 +69,7 @@ kubectl get envoyextensionpolicy \ -o jsonpath='{.items[*].status}' | jq ``` -Look for messages containing `could not fetch Wasm OCI image` in the Envoy proxy logs: +Check Envoy proxy logs for `could not fetch Wasm OCI image`: ```bash kubectl logs -n tigera-gateway \ @@ -77,13 +77,13 @@ kubectl logs -n tigera-gateway \ -c envoy | grep -i wasm ``` -**Fix.** The operator manages the WASM image reference. For clusters in a private registry environment, wire in the appropriate pull secret through the operator configuration. Verify the node can reach the registry and that the pull secret is correctly associated with the Envoy proxy pods. +**Fix.** The operator manages the image reference. In a private registry, wire in the pull secret via operator configuration, and confirm the node can reach the registry with the secret attached to the Envoy proxy pods. --- ## `Programmed=False, Reason=NotAttempted` -**Cause.** A `GlobalWAFValidationPolicy` in `Enforce` mode evaluated the merged configuration and found a `Critical` violation. The controller did not attempt to program the Coraza filter. +**Cause.** A `GlobalWAFValidationPolicy` in `Enforce` mode found a `Critical` violation, so the controller never programmed the Coraza filter. **Diagnostic.** Read the validation failures: @@ -92,15 +92,15 @@ kubectl get wafpolicy -n \ -o jsonpath='{.status.validation}' | jq ``` -The `failures[]` array shows which `GlobalWAFValidationPolicy` rule failed, with its severity and message. The `status.validation.status` field will be `Rejected`. +`failures[]` names the failed rule, with severity and message; `status.validation.status` is `Rejected`. -**Fix.** Correct the `WAFPolicy` to satisfy the failing rule — for example, change `action: Detect` back to `action: Block` if the validation policy requires Block mode. If you believe the policy should be an exception, ask the cluster operator to adjust the `GlobalWAFValidationPolicy`. For how to read and act on validation output, see [Validate configuration with Rego](./common-tasks.mdx#validate-configuration-with-rego) and [Status conditions](./reference.mdx#status-conditions). +**Fix.** Satisfy the failing rule — e.g. restore `action: Block` if the policy requires it — or ask the cluster operator for an exception. See [Validate configuration with Rego](./common-tasks.mdx#validate-configuration-with-rego) and [Status conditions](./reference.mdx#status-conditions). --- ## `Accepted=False, Reason=TargetNotFound` -**Cause.** The `targetRefs` in the `WAFPolicy` reference a `Gateway` or `HTTPRoute` that does not exist in the same namespace. +**Cause.** `targetRefs` references a `Gateway`/`HTTPRoute` that doesn't exist in the namespace. **Fix.** Verify the target exists: @@ -108,25 +108,25 @@ The `failures[]` array shows which `GlobalWAFValidationPolicy` rule failed, with kubectl get gateway,httproute -n ``` -Check that the `name`, `kind`, and `group` in `targetRefs` exactly match the resource. Create the target first, then re-apply the `WAFPolicy`, or correct the reference. Once the target exists the controller will reconcile automatically. +Check `name`, `kind`, `group` in `targetRefs` match the resource, then create it and re-apply, or fix the reference — reconciliation is automatic once it exists. --- ## Attacks not blocked -**Cause.** One or more of the following applies: +**Cause.** One or more of: -- `action` is `Detect` (either explicitly or inherited from `GlobalWAFPolicy`) — traffic is inspected but not blocked. -- `coreRuleSet.state` is `Disabled` — the OWASP Core Rule Set is not loaded. -- `coreRuleSet.paranoiaLevel` is lower than the paranoia floor for the triggering Core Rule Set rule — the rule does not evaluate. +- `action` is `Detect` (explicit or inherited from `GlobalWAFPolicy`) — inspected, not blocked. +- `coreRuleSet.state` is `Disabled` — Core Rule Set isn't loaded. +- `coreRuleSet.paranoiaLevel` is below the triggering rule's paranoia floor — the rule doesn't evaluate. -**Diagnostic.** Check the effective merged configuration. Start with the policy's conditions and spec: +**Diagnostic.** Check the policy's conditions and spec: ```bash kubectl describe wafpolicy -n ``` -Confirm the action is `Block` and Core Rule Set is `Enabled`. If the paranoia level looks correct but the attack still passes, check whether the specific Core Rule Set rule has a higher paranoia floor than the configured level — some rules only activate at paranoia 2 or 3. +Confirm action is `Block` and Core Rule Set `Enabled`. If paranoia looks right but the attack still passes, check whether that rule's floor is higher than configured — some only activate at paranoia 2 or 3. :::tip @@ -134,7 +134,7 @@ Confirm the action is `Block` and Core Rule Set is `Enabled`. If the paranoia le ::: -Also verify the `GlobalWAFPolicy` default action — a namespace `WAFPolicy` that does not set `action` inherits it: +Also check the `GlobalWAFPolicy` default action, inherited when a namespace `action` is unset: ```bash kubectl get globalwafpolicy default -o jsonpath='{.spec.defaultAction}' @@ -148,7 +148,7 @@ See [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont- **Cause.** The paranoia level is too high for the traffic pattern, or a specific Core Rule Set rule is matching legitimate requests (a false positive). -**Diagnostic.** Identify which rule is firing. Check the Envoy proxy access logs in the Gateway pod: +**Diagnostic.** Check the Envoy proxy access logs in the Gateway pod for the firing rule: ```bash kubectl logs -n tigera-gateway \ @@ -156,12 +156,12 @@ kubectl logs -n tigera-gateway \ -c envoy | grep -i "waf\|403" | tail -30 ``` -The log lines will include the rule ID and message that triggered the block. +The log lines include the rule ID and message that triggered the block. **Fix.** Two options: -1. **Lower the paranoia level** if the false positives come from a large class of higher-paranoia rules and your threat model allows it. -2. **Add a plugin exception** to suppress the specific rule ID using `SecRuleRemoveById` or to skip the rule for a known-safe target using `SecRuleUpdateTargetById`. This is the preferred approach for surgical tuning without weakening the overall posture. +1. **Lower the paranoia level**, if false positives come from a broad class of higher-paranoia rules and your threat model allows it. +2. **Add a plugin exception** — suppress via `SecRuleRemoveById`, or skip a known-safe target via `SecRuleUpdateTargetById`. Preferred for surgical tuning without weakening the overall posture. For how to write exception rules within your namespace's ID range, see [Write custom rules with plugins](./common-tasks.mdx#write-custom-rules-with-plugins). @@ -182,8 +182,8 @@ kubectl get wafpolicy -n \ -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}): {.message}{"\n"}{end}' ``` -1. `Licensed=False` → The license is missing or does not entitle Gateway WAF. Check [Get started](./get-started.mdx). -2. `Accepted=False` → The policy spec is rejected. Read the `Reason` and `message` — see the sections above. -3. `Validated=False` → A validation rule fired. Read `status.validation.failures`. -4. `Programmed=False` → The filter was not loaded into Envoy. Read the `Reason` — see `WASMUnavailable` and `NotAttempted` above. -5. All conditions `True` but WAF not blocking → Allow a few seconds for Envoy to load the WASM module after `EnvoyExtensionPolicy` creation. If it still does not block, the effective action is likely `Detect` — see [Attacks not blocked](#attacks-not-blocked) above. +1. `Licensed=False` → license missing or doesn't entitle Gateway WAF. Check [Get started](./get-started.mdx). +2. `Accepted=False` → spec rejected. Read the `Reason`/`message` — see the sections above. +3. `Validated=False` → a validation rule fired. Read `status.validation.failures`. +4. `Programmed=False` → filter not loaded into Envoy. Read the `Reason` — see `WASMUnavailable`/`NotAttempted` above. +5. All conditions `True` but not blocking → give Envoy a few seconds to load the WASM module after `EnvoyExtensionPolicy` creation; if it still doesn't block, the effective action is likely `Detect` — see [Attacks not blocked](#attacks-not-blocked) above. From ea50bf4f49c41f85a612823bfcb993f2b0f2c97e Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 15:34:16 +0100 Subject: [PATCH 10/16] docs(gateway-waf): correct observability, sectionName, and status conditions - Gateway-WAF hits are recorded in tigera_secure_ee_waf (with gateway_name/gateway_namespace) but are not raised as Security Events on this release (intrusion-detection GatewayName filter); re-surfacing is a later-release item. - sectionName is supported: drop the "reserved for a future release" wording and the SectionNameNotYetSupported status reason. - Status-condition reasons corrected to match the reconciler: LicenseRequired (not LicenseBlocked); TargetNotFound and RenderTooLarge under Programmed; add Invalid/ValidationFailed/PluginNotFound under Accepted; drop UnsupportedTarget; Accepted=True reason is Accepted (not Reconciled). Ref: EV-6466 EV-6650 EV-6786 --- .../threat/gateway-waf/common-tasks.mdx | 10 +++------- calico-enterprise/threat/gateway-waf/reference.mdx | 13 +++++++------ .../threat/gateway-waf/troubleshooting.mdx | 10 +++++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/common-tasks.mdx b/calico-enterprise/threat/gateway-waf/common-tasks.mdx index a4c1c0171a..5b9ddfde94 100644 --- a/calico-enterprise/threat/gateway-waf/common-tasks.mdx +++ b/calico-enterprise/threat/gateway-waf/common-tasks.mdx @@ -88,7 +88,7 @@ NAME ACCEPTED AGE default True 30s ``` -Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Reconciled` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). +Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Accepted` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). Every namespace with a `WAFPolicy` inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy` unless it overrides them: @@ -123,7 +123,7 @@ Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` (max 16 entr :::note -`sectionName` (targeting a specific listener on a Gateway) is reserved for a future release. Omit it. +`sectionName` scopes a `targetRefs` entry to a specific listener on a `Gateway` (or a named section), so WAF applies to just that listener rather than the whole Gateway. ::: @@ -181,11 +181,7 @@ Access denied :::tip -In `Detect` mode the request is allowed but the WAF event appears in the Envoy Gateway logs: - -```bash -kubectl -n envoy-gateway-system logs deploy/envoy-gateway --tail=50 | grep -i "waf\|coraza\|modsec" -``` +In `Detect` mode the request is allowed (200), but the WAF hit is still recorded in the `tigera_secure_ee_waf` log index (carrying `gateway_name`/`gateway_namespace`). On this release, gateway-WAF hits are not raised as Security Events — see [Observing WAF activity](./troubleshooting.mdx#observing-waf-activity). ::: diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx index 4828253985..d26ec01636 100644 --- a/calico-enterprise/threat/gateway-waf/reference.mdx +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -69,7 +69,7 @@ spec: | `coreRuleSet.state` | `Enabled` \| `Disabled` | both | Defaults to `Enabled`; nil on WAFPolicy = inherit | | `coreRuleSet.paranoiaLevel` | integer 1–4 | both | Defaults to `1`; nil on WAFPolicy = inherit | | `plugins[]` | list of `{kind, name}` | both | `kind`: `WAFPlugin` \| `GlobalWAFPlugin`; order = array position; max 64 | -| `targetRefs[]` | Gateway API target refs | **WAFPolicy only** | `kind`: `Gateway` \| `HTTPRoute`; min 1, max 16. `sectionName` is reserved for a future release | +| `targetRefs[]` | Gateway API target refs | **WAFPolicy only** | `kind`: `Gateway` \| `HTTPRoute`; min 1, max 16. `sectionName` optionally scopes a ref to a specific listener/section | ### Plugin family @@ -308,7 +308,7 @@ Gateway WAF is a $[prodname] feature and requires a valid license. | Reason | Meaning | | :--- | :--- | | `LicenseValid` | The license entitles this feature | -| `LicenseBlocked` | No valid entitlement; the policy is not programmed | +| `LicenseRequired` | No valid entitlement; the policy is not programmed | ### Accepted @@ -317,13 +317,13 @@ Whether the resource is structurally valid and admitted. Several reasons come st | Reason | Meaning | | :--- | :--- | | `Accepted` | The resource is valid and admitted | +| `Invalid` | The spec or a directive is invalid (the message says which) | +| `ValidationFailed` | A `GlobalWAFValidationPolicy` in `Enforce` mode reported a `Critical` violation | | `Conflicted` | Another policy already targets this route; the older one wins (the message names it) | -| `TargetNotFound` | The referenced `Gateway`/`HTTPRoute` does not exist | -| `UnsupportedTarget` | The `targetRefs` kind is not supported | +| `PluginNotFound` | A referenced `WAFPlugin`/`GlobalWAFPlugin` does not exist | | `UnsafeDirective` | A directive is not on the allowlist | | `IDOutsideRange` | A rule ID is outside the namespace's allocated range | | `ProtectedIDRemoval` | A `SecRuleRemoveById` targets a protected baseline rule | -| `SectionNameNotYetSupported` | `sectionName` was set; per-rule attachment is reserved for a future release | ### Validated @@ -342,6 +342,8 @@ Whether the Coraza WASM filter was programmed into Envoy Gateway. | Reason | Meaning | | :--- | :--- | | `ConfigurationApplied` | The `EnvoyExtensionPolicy` was generated and the filter is active | +| `TargetNotFound` | No valid `targetRefs`, or a referenced `Gateway`/`HTTPRoute` does not exist | +| `RenderTooLarge` | The merged ruleset exceeds the render size limit | | `WASMUnavailable` | The Coraza WASM image could not be pulled | | `NotAttempted` | A `Critical` violation under `Enforce` blocked programming | @@ -353,7 +355,6 @@ A roll-up of the conditions above. | :--- | :--- | | `Ready` | The policy is active on the data plane | | `NotReady` | One of the conditions above is not satisfied | -| `LicenseBlocked` | Blocked on licensing | ### Validation summary diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index eb2432f4dc..f640c5937a 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -12,6 +12,12 @@ kubectl describe wafpolicy -n A healthy policy has `Licensed=True`, `Accepted=True`, `Validated=True`, `Programmed=True`, `Ready=True` — evaluated roughly in order, so an early failure explains later absences. See [Status conditions](./reference.mdx#status-conditions) for reason meanings. +## Observing WAF activity + +WAF hits (both `Block` and `Detect`/`would_block`) are recorded in the `tigera_secure_ee_waf` log index, carrying `gateway_name`/`gateway_namespace` for the originating Gateway. On this release they are **not** raised as Security Events: the intrusion-detection controller excludes WAF logs that carry a gateway name, so gateway-WAF activity is observed through the WAF log index rather than the Security Events view. Re-surfacing gateway WAF as Security Events (or on a dedicated dashboard) is planned for a later release. + +Raw Envoy proxy logs are a last-resort diagnostic when a hit is missing from the index. + --- ## `Accepted=False, Reason=Conflicted` @@ -148,7 +154,7 @@ See [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont- **Cause.** The paranoia level is too high for the traffic pattern, or a specific Core Rule Set rule is matching legitimate requests (a false positive). -**Diagnostic.** Check the Envoy proxy access logs in the Gateway pod for the firing rule: +**Diagnostic.** The firing rule ID and message are in the `tigera_secure_ee_waf` log index for the hit. As a last resort, the Envoy proxy access logs in the Gateway pod also carry them: ```bash kubectl logs -n tigera-gateway \ @@ -156,8 +162,6 @@ kubectl logs -n tigera-gateway \ -c envoy | grep -i "waf\|403" | tail -30 ``` -The log lines include the rule ID and message that triggered the block. - **Fix.** Two options: 1. **Lower the paranoia level**, if false positives come from a broad class of higher-paranoia rules and your threat model allows it. From 1ea2251a04c6c33b0b24ad6f20cbc6da38ad3e29 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 15:57:37 +0100 Subject: [PATCH 11/16] docs(gateway-waf): fix inbound link orphaned by the consolidation The deprecation note on deploying-waf-ingress-gateway.mdx linked to gateway-waf/how-to/attach-waf-to-a-route.mdx, which the 16->6 consolidation removed. Repoint it to the merged Common tasks section (#attach-waf-to-a-gateway-or-route). Ref: EV-6466 --- calico-enterprise/threat/deploying-waf-ingress-gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx index 7289502342..ef75cb6774 100644 --- a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx +++ b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx @@ -15,7 +15,7 @@ This feature is tech preview. Tech preview features may be subject to significan :::caution -This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/how-to/attach-waf-to-a-route.mdx). This page will be removed in a future release. +This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/common-tasks.mdx#attach-waf-to-a-gateway-or-route). This page will be removed in a future release. ::: From a53c8b9a212935604e4591cdba4a3e4a866707e4 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 17:19:30 +0100 Subject: [PATCH 12/16] docs(gateway-waf): reorganize into task-oriented pages Per docs-team feedback, reorganize (not further shrink) the section: - Overview becomes the concepts home: absorbs How it works (resource model, singleton, inheritance, load order, guardrails) and the architecture diagram; the standalone How-it-works page is removed. - Get started slims to a signpost (prerequisites + enable -> tutorial -> tasks); the hands-on walkthrough moves to a new Tutorial page. - Enable/disable becomes its own page (Enable Gateway WAF). - Common tasks split into one page per task (set a baseline, attach WAF, write custom rules, validate config, multi-tenant); persona labels dropped, each gains a what/why lead. - Sidebar reordered to the new layout; inbound links (incl. the deprecation note) repointed. Ref: EV-6466 --- .../threat/deploying-waf-ingress-gateway.mdx | 2 +- .../gateway-waf/attach-waf-to-a-route.mdx | 88 +++ .../threat/gateway-waf/common-tasks.mdx | 530 ------------------ .../threat/gateway-waf/enable.mdx | 83 +++ .../threat/gateway-waf/get-started.mdx | 302 +--------- .../threat/gateway-waf/how-it-works.mdx | 149 ----- .../threat/gateway-waf/index.mdx | 163 +++++- .../threat/gateway-waf/multi-tenant-setup.mdx | 71 +++ .../threat/gateway-waf/reference.mdx | 4 +- .../threat/gateway-waf/set-a-baseline.mdx | 93 +++ .../threat/gateway-waf/troubleshooting.mdx | 8 +- .../threat/gateway-waf/tutorial.mdx | 246 ++++++++ .../threat/gateway-waf/validate-config.mdx | 126 +++++ .../threat/gateway-waf/write-custom-rules.mdx | 130 +++++ sidebars-calico-enterprise.js | 11 +- 15 files changed, 1008 insertions(+), 998 deletions(-) create mode 100644 calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/common-tasks.mdx create mode 100644 calico-enterprise/threat/gateway-waf/enable.mdx delete mode 100644 calico-enterprise/threat/gateway-waf/how-it-works.mdx create mode 100644 calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx create mode 100644 calico-enterprise/threat/gateway-waf/set-a-baseline.mdx create mode 100644 calico-enterprise/threat/gateway-waf/tutorial.mdx create mode 100644 calico-enterprise/threat/gateway-waf/validate-config.mdx create mode 100644 calico-enterprise/threat/gateway-waf/write-custom-rules.mdx diff --git a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx index ef75cb6774..5f91965f11 100644 --- a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx +++ b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx @@ -15,7 +15,7 @@ This feature is tech preview. Tech preview features may be subject to significan :::caution -This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/common-tasks.mdx#attach-waf-to-a-gateway-or-route). This page will be removed in a future release. +This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/attach-waf-to-a-route.mdx). This page will be removed in a future release. ::: diff --git a/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx b/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx new file mode 100644 index 0000000000..8ffefbef45 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx @@ -0,0 +1,88 @@ +--- +description: Attach a WAFPolicy to a Gateway or HTTPRoute to turn on WAF inspection for that target, layering namespace overrides on the cluster baseline. +--- + +# Attach WAF to a Gateway or route + +Create a `WAFPolicy` in your namespace pointing at Gateway API targets — WAF inspection doesn't start automatically when the feature is enabled. It inherits the cluster baseline with namespace overrides layered on top. + +**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy`; you have write access to `WAFPolicy` in your namespace; a `Gateway`/`HTTPRoute` is already receiving traffic. + +`WAFPolicy.spec.targetRefs[]` accepts two kinds of targets: + +| `kind` in `targetRefs` | WAF covers | Use when | +| :--- | :--- | :--- | +| `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | +| `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | + +Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` (max 16 entries). + +:::note + +`sectionName` scopes a `targetRefs` entry to a specific listener on a `Gateway` (or a named section), so WAF applies to just that listener rather than the whole Gateway. + +::: + +Attaching WAF to one `HTTPRoute`, overriding action and paranoia; the unset `coreRuleSet.state` still inherits from the baseline: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-api-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override: Block even if global baseline is Detect + coreRuleSet: + paranoiaLevel: 2 # stricter than the cluster default + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +Apply with `kubectl apply -f wafpolicy.yaml`. For custom rules beyond the baseline, see [Write custom rules](./write-custom-rules.mdx); for inheritance details, see the [Overview](./index.mdx#inheritance-namespaces-override-they-dont-restart). + +## Conflict resolution + +Only one `WAFPolicy` may be active per target. If two in the same namespace target the same Gateway/HTTPRoute, $[prodname] follows [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins; the newer is marked `Accepted=False` reason `Conflicted`, and only the winner's directives are merged. Fix by deleting the newer policy or repointing its `targetRefs`. + +## Verify the policy + +```bash +kubectl get wafpolicy payments-api-waf -n payments +``` + +``` +NAME ACCEPTED PROGRAMMED AGE +payments-api-waf True True 45s +``` + +`.status.conditions` reports `Accepted`, `Programmed`, and validation findings. Confirm Envoy Gateway received the filter with `kubectl get envoyextensionpolicies -n payments` (one per `targetRefs` entry). + +## Test the WAF + +```bash +curl -si "https:///api?id=1+OR+1%3D1" +``` + +``` +HTTP/1.1 403 Forbidden +content-length: 11 +content-type: text/plain + +Access denied +``` + +:::tip + +In `Detect` mode the request is allowed (200), but the WAF hit is still recorded in the `tigera_secure_ee_waf` log index (carrying `gateway_name`/`gateway_namespace`). On this release, gateway-WAF hits are not raised as Security Events — see [Observing WAF activity](./troubleshooting.mdx#observing-waf-activity). + +::: + +:::caution + +`ACCEPTED=False` reason `ValidationFailed` means a `GlobalWAFValidationPolicy` is blocking your config — the status message names the policy and rule. Contact your cluster operator or see [Validate configuration with Rego](./validate-config.mdx). + +::: diff --git a/calico-enterprise/threat/gateway-waf/common-tasks.mdx b/calico-enterprise/threat/gateway-waf/common-tasks.mdx deleted file mode 100644 index 5b9ddfde94..0000000000 --- a/calico-enterprise/threat/gateway-waf/common-tasks.mdx +++ /dev/null @@ -1,530 +0,0 @@ ---- -description: Set a cluster-wide WAF baseline, attach WAF to a Gateway or route, write custom rules, validate configuration with Rego, and run a full multi-tenant setup. ---- - -# Common tasks - -## Set a cluster-wide baseline - -:::note[Cluster operator task] - -Owns the `GlobalWAFPolicy` singleton and sets the baseline every namespace inherits. - -::: - -**Prerequisites:** the Gateway WAF feature gate is enabled on `ApplicationLayer` (see [Get started](./get-started.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. - -### Create the GlobalWAFPolicy singleton - -Must be named `default` — other names are rejected by the admission webhook. Start in `Detect` mode: it logs matches without blocking, so you can measure false positives before switching to `Block`. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Detect # start here; promote to Block after validating false-positive rate - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` - -Apply with `kubectl apply -f globalwafpolicy.yaml`. - -Four paranoia levels; higher catches more attacks at the cost of more false positives: - -| Level | Posture | Typical use | -| :--- | :--- | :--- | -| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | -| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | -| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | -| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | - -:::tip - -Start at level 1 cluster-wide; let application operators raise it per namespace, and enforce a minimum later via a validation policy. - -::: - -Once satisfied, set `defaultAction: Block` — $[prodname] then returns HTTP 403 for matching requests. - -### Add organization-wide rules with a GlobalWAFPlugin - -A `GlobalWAFPlugin` holds custom ModSecurity rules applied cluster-wide via the global policy's `plugins[]` list. Create it, then reference it — rules use the four lifecycle slots in [Write custom rules](#write-custom-rules-with-plugins). - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPlugin -metadata: - name: org-baseline -spec: - description: "Organization-wide security baseline rules" - config: | - SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" - before: | - SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ - "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" - SecRule TX:ENFORCE_HTTPS "@eq 1" "" - rules: | - SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ - "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" - after: | - # Cluster operator's after slot runs last in the merge order. - SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ - "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" -``` - -Reference it from `GlobalWAFPolicy.spec.plugins[]` (`plugins: [{kind: GlobalWAFPlugin, name: org-baseline}]`). `plugins[]` is ordered by array position, up to 64 entries; `WAFPlugin` entries can be added here too. - -### Verify the baseline - -```bash -kubectl get globalwafpolicy default -``` - -``` -NAME ACCEPTED AGE -default True 30s -``` - -Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Accepted` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). - -Every namespace with a `WAFPolicy` inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy` unless it overrides them: - -``` -GlobalWAFPolicy (baseline) - └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 - └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy -``` - ---- - -## Attach WAF to a Gateway or route - -:::note[Application operator task] - -Owns namespace-scoped WAF resources and decides which of their Gateways and routes get WAF inspection. - -::: - -Create a `WAFPolicy` in your namespace pointing at Gateway API targets — WAF inspection doesn't start automatically when the feature is enabled. It inherits the cluster baseline with namespace overrides layered on top. - -**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy`; you have write access to `WAFPolicy` in your namespace; a `Gateway`/`HTTPRoute` is already receiving traffic. - -`WAFPolicy.spec.targetRefs[]` accepts two kinds of targets: - -| `kind` in `targetRefs` | WAF covers | Use when | -| :--- | :--- | :--- | -| `Gateway` | Every `HTTPRoute` bound to that Gateway | You want uniform WAF across all routes on a Gateway | -| `HTTPRoute` | Just that one route | You need per-route tuning — different paranoia level, different plugins | - -Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` (max 16 entries). - -:::note - -`sectionName` scopes a `targetRefs` entry to a specific listener on a `Gateway` (or a named section), so WAF applies to just that listener rather than the whole Gateway. - -::: - -Attaching WAF to one `HTTPRoute`, overriding action and paranoia; the unset `coreRuleSet.state` still inherits from the baseline: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-api-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block # override: Block even if global baseline is Detect - coreRuleSet: - paranoiaLevel: 2 # stricter than the cluster default - # coreRuleSet.state is unset → inherited from GlobalWAFPolicy -``` - -Apply with `kubectl apply -f wafpolicy.yaml`. For custom rules beyond the baseline, see [Write custom rules](#write-custom-rules-with-plugins); for inheritance details, see [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart). - -### Conflict resolution - -Only one `WAFPolicy` may be active per target. If two in the same namespace target the same Gateway/HTTPRoute, $[prodname] follows [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins; the newer is marked `Accepted=False` reason `Conflicted`, and only the winner's directives are merged. Fix by deleting the newer policy or repointing its `targetRefs`. - -### Verify the policy - -```bash -kubectl get wafpolicy payments-api-waf -n payments -``` - -``` -NAME ACCEPTED PROGRAMMED AGE -payments-api-waf True True 45s -``` - -`.status.conditions` reports `Accepted`, `Programmed`, and validation findings. Confirm Envoy Gateway received the filter with `kubectl get envoyextensionpolicies -n payments` (one per `targetRefs` entry). - -### Test the WAF - -```bash -curl -si "https:///api?id=1+OR+1%3D1" -``` - -``` -HTTP/1.1 403 Forbidden -content-length: 11 -content-type: text/plain - -Access denied -``` - -:::tip - -In `Detect` mode the request is allowed (200), but the WAF hit is still recorded in the `tigera_secure_ee_waf` log index (carrying `gateway_name`/`gateway_namespace`). On this release, gateway-WAF hits are not raised as Security Events — see [Observing WAF activity](./troubleshooting.mdx#observing-waf-activity). - -::: - -:::caution - -`ACCEPTED=False` reason `ValidationFailed` means a `GlobalWAFValidationPolicy` is blocking your config — the status message names the policy and rule. Contact your cluster operator or see [Validate configuration with Rego](#validate-configuration-with-rego). - -::: - ---- - -## Write custom rules with plugins - -:::note[Application operator task] - -Cluster operators wanting org-wide rules use `GlobalWAFPlugin` instead, referenced from `GlobalWAFPolicy.spec.plugins[]` — same fields, different scope. - -::: - -A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives in four lifecycle slots, modeled on the Core Rule Set 4 plugin structure. Slot choice follows [load order](./how-it-works.mdx#the-merged-ruleset-load-order): - -| Slot | Runs | Use for | -| :--- | :--- | :--- | -| `config` | Before Core Rule Set setup | `tx.*` variable setup (anomaly thresholds, optional features, app flags). Rules on request variables belong in `before` instead — the request isn't read yet | -| `before` | After variable setup, before Core Rule Set loads | Early-exit allowlisting of trusted paths, request normalization, flags for later rules | -| `rules` | Alongside Core Rule Set, after it (Tigera extension) | App-specific detections that complement rather than pre-empt the Core Rule Set; can act on anomaly scores | -| `after` | Last | False-positive suppression, response inspection, logging. `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, so the cluster operator has final say | - -:::caution - -`ctl:ruleEngine=Off` inside a chained rule or per-request disables the engine for that transaction only. A top-level `SecRuleEngine Off` is rejected by the admission webhook. - -::: - -:::note - -`SecRuleRemoveById`/`ctl:ruleRemoveById` in `after` apply for the rest of phase processing — use to suppress your own false positives. Protected Core Rule Set baseline rules can't be removed from any slot; the webhook rejects the attempt. - -::: - -### Rule IDs and your namespace's range - -Every `SecRule`/`SecAction` needs an ID. Each namespace gets a 10,000-ID block, enforced at admission. Find your range first: - -```bash -kubectl get namespace \ - -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' -# example output: 420000-429999 -``` - -Populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. - -### Worked example - -This `WAFPlugin` uses all four slots to protect a GraphQL API: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: graphql-waf - namespace: platform -spec: - description: "GraphQL introspection guard and false-positive exceptions" - - config: | - # Flag the application type so later rules can branch on it. - SecAction \ - "id:420001,phase:1,pass,nolog,\ - setvar:tx.is_graphql=1" - - before: | - # Allowlist the schema endpoint used by the internal schema registry — - # skip WAF analysis entirely for that specific path. - SecRule REQUEST_URI "@streq /graphql/schema-registry" \ - "id:420010,phase:1,pass,nolog,\ - ctl:ruleEngine=Off,\ - tag:allowlist" - - rules: | - # Block introspection queries (production guard). - SecRule TX:IS_GRAPHQL "@eq 1" \ - "id:420020,phase:2,deny,status:403,\ - msg:'GraphQL introspection blocked',\ - logdata:'body_snippet=%{MATCHED_VAR}',\ - chain" - SecRule REQUEST_BODY "@rx __schema|__type" "" - - # Block suspicious user-agents known to scan GraphQL APIs. - SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ - "id:420021,phase:1,deny,status:403,\ - msg:'Known GraphQL scanner blocked',\ - logdata:'ua=%{MATCHED_VAR}'" - - after: | - # Remove a false positive: CRS rule 941130 fires on a legitimate - # Content-Type value used by our GraphQL client library. - SecRuleRemoveById 941130 -``` - -:::tip - -`@pmf` loads a data file, which must be bundled into the Coraza WASM filter image — ask your cluster operator, or use `@pm` (inline phrase match) for a short string list. - -::: - -A plugin has no effect until a `WAFPolicy` references it, in `spec.plugins[]` (load order = array position): - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: platform-api-waf - namespace: platform -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: graphql-api - action: Block - plugins: - - kind: WAFPlugin - name: graphql-waf # ← your plugin above -``` - -If the policy also references a `GlobalWAFPlugin`, its slots load before the namespace plugin's slots at each position. - -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. - -:::note - -Not all SecLang directives are permitted — directives that read the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. - -::: - -Confirm acceptance and reconciliation: - -```bash -kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" -kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" -``` - -`Ready=True` on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed. - ---- - -## Validate configuration with Rego - -Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang at apply time (structural safety); validation policies check the **merged result** at reconcile time, catching problems only visible once everything's merged. See [Validation and enforcement](./how-it-works.mdx#validation-and-enforcement) for the matrix and [Rego validation](./reference.mdx#rego-validation) for the input schema. - -### Cluster operator: GlobalWAFValidationPolicy - -:::note[Cluster operator] - -Cluster-scoped; applies to every namespace or a label-selected subset via `namespaceSelector`. - -::: - -Carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. - -This policy enforces three requirements for namespaces labeled `env: production`, and audits a fourth: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: production-requirements -spec: - enforcementMode: Enforce - namespaceSelector: - matchLabels: - env: production - rules: - - # Critical: CRS must be enabled - - name: crs-must-be-enabled - severity: Critical - rego: | - package waf - violations[{"msg": "OWASP CRS must be enabled"}] { - input.config.crsState != "Enabled" - } - - # Critical: production namespaces must use Block mode - - name: block-mode-required - severity: Critical - rego: | - package waf - violations[{"msg": "WAF must be in Block mode for production"}] { - input.config.action != "Block" - } - - # Critical: minimum paranoia level - - name: min-paranoia-level - severity: Critical - rego: | - package waf - violations[{"msg": msg}] { - input.config.paranoiaLevel < 2 - msg := sprintf( - "paranoia level %d is below the production minimum of 2", - [input.config.paranoiaLevel] - ) - } - - # Warning: advise against disabling RCE rules (does not block) - - name: rce-rules-intact - severity: Warning - rego: | - package waf - violations[{"msg": msg}] { - d := input.directives[_] - startswith(d, "SecRuleRemoveById") - re_match(`93[0-9]{4}`, d) - msg := sprintf("RCE rule removal detected in merged config: %s", [d]) - } -``` - -:::tip - -Start new policies in `Audit` mode; review violations on existing `WAFPolicy` status before switching to `Enforce`, so you don't block teams whose config predates the policy. - -::: - -Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. - -### Application operator: WAFValidationPolicy - -:::note[Application operator] - -Namespace-scoped and advisory only — violations appear on status but never block programming. Use as a self-check. - -::: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFValidationPolicy -metadata: - name: team-self-check - namespace: platform -spec: - rules: - - - name: graphql-plugin-active - severity: Warning - rego: | - package waf - violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { - not "graphql-waf" in input.source.namespacePlugins - } -``` - -Being advisory, it's safe to hold yourself to a stricter standard than the baseline — e.g. a higher minimum paranoia level. Violations surface on `WAFPolicy` status without interrupting traffic. - -### Reading violation results - -Violations from matching policies appear in the affected `WAFPolicy`'s status conditions. Check with `kubectl get wafpolicy -n -o yaml`. A blocked policy: - -```yaml -status: - conditions: - - type: Ready - status: "False" - reason: ValidationFailed - message: > - GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": - WAF must be in Block mode for production - - type: Accepted - status: "True" -``` - -An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. - -:::tip - -Write Rego against the schema in [Rego validation](./reference.mdx#rego-validation); test locally with `opa eval` first. A syntax error invalidates the whole policy — status reports which rule failed to parse. - -::: - ---- - -## Multi-tenant setup - -:::note[Scenario] - -A cluster shared by `team-a` and `payments`, with a cluster operator setting the baseline via a `GlobalWAFPolicy`, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that stops teams weakening protection. Each team attaches WAF to its own Gateway and tunes independently. - -**Prerequisites:** Gateway WAF enabled; valid license. - -::: - -The cluster operator creates the `default` `GlobalWAFPolicy`, `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` (`Enforce`, scoped to `env: production`) — per the patterns above. Application operators need read access to `Global*` resources to see what they're inheriting: - -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: waf-global-reader -rules: - - apiGroups: ["applicationlayer.projectcalico.org"] - resources: - - globalwafpolicies - - globalwafplugins - - globalwafvalidationpolicies - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: waf-global-reader - namespace: team-a # repeat for payments -subjects: - - kind: Group - name: team-a-operators - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: ClusterRole - name: waf-global-reader - apiGroup: rbac.authorization.k8s.io -``` - -:::note - -Grants read on cluster-scoped resources via a namespace-scoped `RoleBinding` — application operators can see but not edit `Global*` resources. - -::: - -Each team labels its namespace (`kubectl label namespace team-a env=production`, same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. `team-a` raises paranoia to 2, inheriting `coreRuleSet.state: Enabled`; `payments` runs paranoia 3 plus a namespace `WAFPlugin` blocking GraphQL introspection, using an ID from its own [SecRule ID range](./reference.mdx#secrule-id-ranges). - -### What happens if a team tries to weaken the baseline - -If `payments` applies `action: Detect`, `production-requirements` (`Enforce`) fires its `must-use-block-mode` Critical rule — the controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation: - -```bash -kubectl get wafpolicy payments-waf -n payments \ - -o jsonpath='{.status.validation.failures}' | jq -``` - -No `EnvoyExtensionPolicy` is generated until corrected — restore `action: Block` or request an exception from the cluster operator. - -### Result: isolation in practice - -- **`team-a`** — Core Rule Set at paranoia 2, own custom rules, own ID range. -- **`payments`** — Core Rule Set at paranoia 3, GraphQL introspection block, a different ID range. -- Neither team can disable the org-wide rule from `GlobalWAFPlugin` or drop below Block mode. -- The cluster operator sees all violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — one place to see which namespaces are non-compliant. diff --git a/calico-enterprise/threat/gateway-waf/enable.mdx b/calico-enterprise/threat/gateway-waf/enable.mdx new file mode 100644 index 0000000000..66673ea01b --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/enable.mdx @@ -0,0 +1,83 @@ +--- +description: Turn Gateway WAF on or off by setting the operator GatewayAPI CR's extensions.waf.state field. +--- + +# Enable Gateway WAF + +Gateway WAF is off by default. Turning it on is a one-line change to the operator's `GatewayAPI` CR — no new workload is deployed, so there's no extra resource footprint to plan for. + +## Enable Gateway WAF + +Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. + +```yaml +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled # default is Disabled +``` + +Apply it: + +```bash +kubectl apply -f - <<'EOF' +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Enabled +EOF +``` + +The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. + +Confirm the six CRDs are installed: + +```bash +kubectl get crd | grep applicationlayer.projectcalico.org +``` + +```text +globalwafplugins.applicationlayer.projectcalico.org +globalwafpolicies.applicationlayer.projectcalico.org +globalwafvalidationpolicies.applicationlayer.projectcalico.org +wafplugins.applicationlayer.projectcalico.org +wafpolicies.applicationlayer.projectcalico.org +wafvalidationpolicies.applicationlayer.projectcalico.org +``` + +All six CRDs must be present before you create any WAF resources. + +:::tip + +To confirm the admission webhook is active, run `kubectl get validatingwebhookconfigurations | grep waf`. You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. + +::: + +## Disable Gateway WAF + +Set `spec.extensions.waf.state` back to `Disabled` on the same `GatewayAPI` CR to turn the feature off: + +```yaml +apiVersion: operator.tigera.io/v1 +kind: GatewayAPI +metadata: + name: default +spec: + extensions: + waf: + state: Disabled +``` + +The operator removes the admission webhook and stops the WAF reconcilers; existing `WAFPolicy`, `GlobalWAFPolicy`, and other WAF resources stay on the cluster but are no longer reconciled or enforced until you re-enable the feature. + +## Next steps + +- [Tutorial](./tutorial.mdx) — deploy a test app and block your first attack. diff --git a/calico-enterprise/threat/gateway-waf/get-started.mdx b/calico-enterprise/threat/gateway-waf/get-started.mdx index 73c33236e6..8011762beb 100644 --- a/calico-enterprise/threat/gateway-waf/get-started.mdx +++ b/calico-enterprise/threat/gateway-waf/get-started.mdx @@ -1,5 +1,5 @@ --- -description: Enable Gateway WAF on the GatewayAPI operator CR, then deploy a test app and block a SQL injection and XSS attack in about five minutes. +description: Prerequisites for Gateway WAF, and the path from enabling the feature through the tutorial to production rollout. --- # Get started @@ -10,7 +10,7 @@ This feature is tech preview. Tech preview features may be subject to significan ::: -This guide takes you from a running Ingress Gateway to a blocked attack in about five minutes. +Gateway WAF protects traffic through $[prodname] Ingress Gateway with a per-route, per-namespace web application firewall: a cluster operator sets a baseline once, and application operators layer their own rules on top. This page gets you oriented — what you need before you start, and where to go next. ## Before you begin @@ -18,7 +18,7 @@ This guide takes you from a running Ingress Gateway to a blocked attack in about - $[prodname] is installed with a valid license that includes the Gateway WAF feature. - $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). -- You have a namespace where you can deploy a test application. +- You have a namespace where you can deploy a test application (needed for the tutorial). :::note @@ -26,296 +26,10 @@ Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires th ::: -## Step 1: Enable the feature +## The path -Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. +1. [Enable the feature](./enable.mdx) — turn on Gateway WAF on the operator `GatewayAPI` CR. +2. [Try the tutorial](./tutorial.mdx) — deploy a test app and block a SQL injection and XSS attack in about five minutes. +3. [Move to production](./set-a-baseline.mdx) — set a cluster-wide baseline, then [attach WAF](./attach-waf-to-a-route.mdx), [write custom rules](./write-custom-rules.mdx), [validate configuration](./validate-config.mdx), and set up [multi-tenant](./multi-tenant-setup.mdx) ownership. -```yaml -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled # default is Disabled -``` - -Apply it: - -```bash -kubectl apply -f - <<'EOF' -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled -EOF -``` - -The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. - -Confirm the six CRDs are installed: - -```bash -kubectl get crd | grep applicationlayer.projectcalico.org -``` - -```text -globalwafplugins.applicationlayer.projectcalico.org -globalwafpolicies.applicationlayer.projectcalico.org -globalwafvalidationpolicies.applicationlayer.projectcalico.org -wafplugins.applicationlayer.projectcalico.org -wafpolicies.applicationlayer.projectcalico.org -wafvalidationpolicies.applicationlayer.projectcalico.org -``` - -All six CRDs must be present before you create any WAF resources. - -:::tip - -To confirm the admission webhook is active, run `kubectl get validatingwebhookconfigurations | grep waf`. You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. - -::: - -## Step 2: Deploy a test application - -Create an `httpbin` namespace with a deployment and service to act as the backend. - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: httpbin ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: httpbin - namespace: httpbin -spec: - replicas: 1 - selector: - matchLabels: - app: httpbin - template: - metadata: - labels: - app: httpbin - spec: - containers: - - name: httpbin - image: kong/httpbin:0.1.0 - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: httpbin - namespace: httpbin -spec: - selector: - app: httpbin - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Apply it, then wait for the pod to be ready: - -```bash -kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s -``` - -## Step 3: Create a Gateway and HTTPRoute - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: httpbin-gateway - namespace: httpbin -spec: - gatewayClassName: tigera-gateway-class - listeners: - - name: http - protocol: HTTP - port: 9080 ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: httpbin-route - namespace: httpbin -spec: - parentRefs: - - name: httpbin-gateway - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: httpbin - port: 80 -``` - -:::note - -Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod. - -::: - -Apply it, then wait for the Gateway to be accepted: - -```bash -kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s -``` - -## Step 4: Confirm traffic flows - -Find the Envoy service that the Ingress Gateway created for this Gateway. Services are created in the `tigera-gateway` namespace. - -```bash -export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ - -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') -``` - -Send a test request from inside the cluster: - -```bash -kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get -``` - -```text -200 -``` - -Traffic is flowing through the Gateway with no WAF protection yet. - -## Step 5: Apply a GlobalWAFPolicy - -:::tip - -This quickstart uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [How it works](./how-it-works.mdx#merge-order-and-enforcement) for the tradeoffs. - -::: - -`GlobalWAFPolicy` sets the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Block - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` - -This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. - -## Step 6: Attach WAF to the Gateway - -Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: httpbin-waf - namespace: httpbin -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: httpbin-gateway -``` - -The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. - -Check that the policy was accepted and programmed: - -```bash -kubectl get wafpolicy httpbin-waf -n httpbin \ - -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' -``` - -```text -Licensed=True (LicenseValid) -Accepted=True (Accepted) -Validated=Unknown (NotEvaluated) -Programmed=True (Programmed) -Ready=True (Ready) -``` - -All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. - -If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](./troubleshooting.mdx). - -Allow a few seconds for Envoy to load the WASM filter before running the tests below. - -## Step 7: Test WAF blocking - -**Normal request — expect 200:** - -```bash -kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get -``` - -```text -200 -``` - -**SQL injection probe — expect 403:** - -```bash -kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" -``` - -```text -403 -``` - -**XSS probe — expect 403:** - -```bash -kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" -``` - -```text -403 -``` - -The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. - -## Clean up - -```bash -kubectl delete namespace httpbin -kubectl delete globalwafpolicies default -``` - -## Troubleshoot - -If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](./troubleshooting.mdx) for diagnosis steps and the pull-secret configuration. - -## Next steps - -- [Common tasks](./common-tasks.mdx) — set a cluster-wide baseline, write custom rules, validate configuration, and run a full multi-tenant setup. -- [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. +For concepts — resource families, merge order, and enforcement — see the [Overview](./index.mdx). If something isn't working as expected, [Troubleshooting](./troubleshooting.mdx) walks through common failure modes. diff --git a/calico-enterprise/threat/gateway-waf/how-it-works.mdx b/calico-enterprise/threat/gateway-waf/how-it-works.mdx deleted file mode 100644 index adaca3c7e0..0000000000 --- a/calico-enterprise/threat/gateway-waf/how-it-works.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -description: How Gateway WAF resources are organized, how cluster-scoped and namespace-scoped configuration relate, and how the merged ruleset is assembled and enforced. ---- - -# How it works - -Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group, in three families. Each has a cluster-scoped resource owned by the **cluster operator (CO)** and a namespace-scoped resource owned by the **application operator (AO)**. - -| Family | Cluster-scoped (CO) | Namespace-scoped (AO) | Answers the question | -| :--- | :--- | :--- | :--- | -| **Policy** | `GlobalWAFPolicy` | `WAFPolicy` | *What* to enforce — action, Core Rule Set state, paranoia, which plugins | -| **Plugin** | `GlobalWAFPlugin` | `WAFPlugin` | *Which custom rules* to add, in ModSecurity SecLang | -| **Validation** | `GlobalWAFValidationPolicy` | `WAFValidationPolicy` | *Whether a configuration is allowed* — compliance guardrails | - -The cluster operator *sets* the posture; the application operator *tunes* it — can go stricter, but can be stopped from going weaker (see [Validation and enforcement](#validation-and-enforcement)). - -## Policies — what to enforce - -A **policy** declares the action mode (`Detect`/`Block`), whether the Core Rule Set is enabled, the paranoia level, and which plugins to load. - -- `GlobalWAFPolicy` — cluster-wide singleton (`name: default`), sets the baseline for every namespace. -- `WAFPolicy` — namespace-scoped, attaches WAF to a Gateway/`HTTPRoute` via `targetRefs`, can override the baseline action and paranoia. - -## Plugins — custom rules - -A **plugin** holds custom ModSecurity rules in four lifecycle slots — `config`, `before`, `rules`, `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Referenced from a policy's `spec.plugins[]`; does nothing on its own. - -- `GlobalWAFPlugin` — organization-wide rules: security baselines, compliance rules. -- `WAFPlugin` — application-specific rules and false-positive exceptions. - -## Validation policies — guardrails - -A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated against the *merged, effective* configuration — answering "is this acceptable?", e.g. "Core Rule Set must stay enabled" or "production must run in Block mode." - -- `GlobalWAFValidationPolicy` — enforced by the cluster operator: `Enforce` mode blocks programming on a `Critical` violation, `Audit` mode only warns. `namespaceSelector` scopes which namespaces it applies to. -- `WAFValidationPolicy` — advisory only, surfaces violations on status for the application operator to act on, never blocks. No enforce mode at namespace scope. - -(Validation policies aren't the admission webhook — see [Validation and enforcement](#validation-and-enforcement) for the distinction.) - -## The global policy is a singleton - -`GlobalWAFPolicy` is a cluster-wide singleton — must be named `default`. It sets the baseline (default action, Core Rule Set state, paranoia level) every namespace inherits unless it overrides. - -`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons — create as many as needed. - -## Inheritance: namespaces override, they don't restart - -A `WAFPolicy` doesn't have to restate the whole baseline — its `action` and `coreRuleSet` fields are optional; unset fields inherit from `GlobalWAFPolicy`. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block # override the global default for this namespace - coreRuleSet: - paranoiaLevel: 2 # stricter than the baseline - # coreRuleSet.state is unset → inherited from GlobalWAFPolicy -``` - -| `WAFPolicy` field | When set | When unset | -| :--- | :--- | :--- | -| `action` | overrides the global `defaultAction` for this namespace | inherits `GlobalWAFPolicy.spec.defaultAction` | -| `coreRuleSet.state` | overrides the global state | inherits the global state | -| `coreRuleSet.paranoiaLevel` | overrides the global level | inherits the global level | - -## Plugins are additive, not overriding - -Plugins stack rather than override. The merger applies all referenced global plugins first (`GlobalWAFPolicy.spec.plugins[]` order), then all namespace plugins (`WAFPolicy.spec.plugins[]` order) — order is array position; there's no priority field. - -This lets baseline rules and application exceptions coexist in one ruleset — the interleaving, and why global `after` runs last, is covered below. - -## Merge order and enforcement - -When a `WAFPolicy` attaches to a target, the controller gathers every applicable policy and plugin, merges them into one ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each `targetRefs[]` entry produces its own `EnvoyExtensionPolicy` — three routes means three filters. - -Three things govern the outcome: **merge order**, the **admission webhook**, and **validation policies**. - -### The merged ruleset load order - -Directive order isn't cosmetic: the Core Rule Set needs variables set before loading, and post-processing after. Fixed order: - -**Engine setup** -1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. -2. Engine settings — audit logging and request-body access. - -**Plugin `config` (variable setup)** -3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. -4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. - -**Core Rule Set setup** -5. Core Rule Set setup — reads the `tx.*` variables set above. -6. Blocking paranoia level, from the merged paranoia level. -7. Detection paranoia level (always ≥ blocking level). - -**Pre-Core Rule Set rules (`before`)** -8. Global plugin `before` rules. -9. Namespace plugin `before` rules. - -**Core Rule Set** -10. The full OWASP Core Rule Set. - -**Custom rules (`rules`)** -11. Global plugin `rules`. -12. Namespace plugin `rules`. - -**Post-processing (`after`)** -13. Namespace plugin `after` rules. -14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. - -Pattern: global-before-namespace in the early slots, namespace-before-global in `after`. Within a plugin, its own slots always run `config` → `before` → `rules` → `after`. - -:::note - -When two `WAFPolicy` resources target the same route, $[prodname] follows Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins (ties by `namespace/name`). The loser is marked `Accepted=False` reason `Conflicted`, directives not merged. - -::: - -### Structural safety: the admission webhook - -Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at apply time, rejecting the resource if any directive fails one of three checks: - -| Check | Passes when | Rejected with | -| :--- | :--- | :--- | -| Directive is allowed | the directive type is on the allowlist (`SecRule`, `SecAction`, `SecRuleRemoveById`, and other safe directives) | `UnsafeDirective` | -| Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | -| Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | - -This stops one namespace colliding with another's rule IDs, or disabling the cluster's baseline Core Rule Set rules. Each namespace gets an automatic 10,000-ID range; see [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for how it's derived and which IDs are protected. - -### Validation and enforcement - -The admission webhook checks *raw SecLang*; validation policies check the *merged result*, running after the merge because some problems only show up there. Two innocent plugins can combine to disable all SQL-injection rules — invisible to admission-time checks, but visible to a Rego rule against the merged directive list. - -Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with severity `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: - -| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | -| :--- | :--- | :--- | :--- | -| `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | -| `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | -| `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | - -To write the rules, see [Rego validation](./reference.mdx#rego-validation). To read the outcome, see [Status conditions](./reference.mdx#status-conditions). diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/index.mdx index 9750592528..93d77ae881 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/index.mdx @@ -1,5 +1,5 @@ --- -description: Protect Calico Ingress Gateway traffic with a per-route and per-namespace web application firewall, built on the OWASP Core Rule Set and delivered as a Coraza WebAssembly filter on Envoy Gateway. +description: Protect Calico Ingress Gateway traffic with a per-route and per-namespace web application firewall built on the OWASP Core Rule Set — how the resource families, merge order, and enforcement guardrails fit together. --- # Gateway WAF @@ -33,24 +33,157 @@ The inputs to the merge are `GlobalWAFPolicy` + `WAFPolicy` and `GlobalWAFPlugin ## Who does what -Gateway WAF splits cleanly across two personas. Cluster-scoped resources belong to the **cluster operator (CO)**; namespace-scoped resources belong to the **application operator (AO)**. +Gateway WAF is configured through six custom resources in the `applicationlayer.projectcalico.org/v3` API group, in three families. Each family splits cleanly across two personas: a cluster-scoped resource owned by the **cluster operator (CO)**, and a namespace-scoped resource owned by the **application operator (AO)**. -| Task | Cluster operator (CO) | Application operator (AO) | +| Family | Cluster operator (CO) | Application operator (AO) | Answers the question | +| :--- | :--- | :--- | :--- | +| **Policy** | `GlobalWAFPolicy` — cluster-wide baseline (singleton, `name: default`) | `WAFPolicy` — attach WAF to a Gateway/`HTTPRoute` (`targetRefs`); per-namespace overrides | *What* to enforce — action, Core Rule Set state, paranoia, which plugins | +| **Plugin** | `GlobalWAFPlugin` — organization-wide custom rules | `WAFPlugin` — app-specific rules and false-positive exceptions | *Which custom rules* to add, in ModSecurity SecLang | +| **Validation** | `GlobalWAFValidationPolicy` — compliance guardrails (`Enforce`/`Audit`) | `WAFValidationPolicy` — advisory self-check, never blocks | *Whether a configuration is allowed* — compliance guardrails | + +Enabling the feature itself (license plus the `GatewayAPI` gate) is a one-time cluster operator step — see [Enable the feature](./enable.mdx) — everything above is ongoing configuration once it's on. + +The cluster operator *sets* the posture; the application operator *tunes* it — can go stricter, but can be stopped from going weaker (see [Validation and enforcement](#validation-and-enforcement)). + +## Policies — what to enforce + +A **policy** declares the action mode (`Detect`/`Block`), whether the Core Rule Set is enabled, the paranoia level, and which plugins to load. + +- `GlobalWAFPolicy` — cluster-wide singleton (`name: default`), sets the baseline for every namespace. +- `WAFPolicy` — namespace-scoped, attaches WAF to a Gateway/`HTTPRoute` via `targetRefs`, can override the baseline action and paranoia. + +## Plugins — custom rules + +A **plugin** holds custom ModSecurity rules in four lifecycle slots — `config`, `before`, `rules`, `after` — modeled on the [Core Rule Set 4 plugin system](https://coreruleset.org/docs/4-about-plugins/4-1-plugins/). Referenced from a policy's `spec.plugins[]`; does nothing on its own. + +- `GlobalWAFPlugin` — organization-wide rules: security baselines, compliance rules. +- `WAFPlugin` — application-specific rules and false-positive exceptions. + +## Validation policies — guardrails + +A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated against the *merged, effective* configuration — answering "is this acceptable?", e.g. "Core Rule Set must stay enabled" or "production must run in Block mode." + +- `GlobalWAFValidationPolicy` — enforced by the cluster operator: `Enforce` mode blocks programming on a `Critical` violation, `Audit` mode only warns. `namespaceSelector` scopes which namespaces it applies to. +- `WAFValidationPolicy` — advisory only, surfaces violations on status for the application operator to act on, never blocks. No enforce mode at namespace scope. + +(Validation policies aren't the admission webhook — see [Validation and enforcement](#validation-and-enforcement) for the distinction.) + +## The global policy is a singleton + +`GlobalWAFPolicy` is a cluster-wide singleton — must be named `default`. It sets the baseline (default action, Core Rule Set state, paranoia level) every namespace inherits unless it overrides. + +`GlobalWAFPlugin` and `GlobalWAFValidationPolicy` are **not** singletons — create as many as needed. + +## Inheritance: namespaces override, they don't restart + +A `WAFPolicy` doesn't have to restate the whole baseline — its `action` and `coreRuleSet` fields are optional; unset fields inherit from `GlobalWAFPolicy`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: payments-waf + namespace: payments +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override the global default for this namespace + coreRuleSet: + paranoiaLevel: 2 # stricter than the baseline + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy +``` + +| `WAFPolicy` field | When set | When unset | | :--- | :--- | :--- | -| Enable the feature (license + `GatewayAPI` gate) | ✓ | | -| Cluster-wide baseline — action, Core Rule Set, paranoia | `GlobalWAFPolicy` (singleton) | | -| Organization-wide custom rules | `GlobalWAFPlugin` | | -| Compliance guardrails (audit or enforce) | `GlobalWAFValidationPolicy` | | -| Attach WAF to a Gateway or `HTTPRoute` | | `WAFPolicy` (`targetRefs`) | -| Per-namespace overrides (action, paranoia) | | `WAFPolicy` | -| App-specific rules and false-positive exceptions | | `WAFPlugin` | -| Self-check namespace config (advisory) | | `WAFValidationPolicy` | +| `action` | overrides the global `defaultAction` for this namespace | inherits `GlobalWAFPolicy.spec.defaultAction` | +| `coreRuleSet.state` | overrides the global state | inherits the global state | +| `coreRuleSet.paranoiaLevel` | overrides the global level | inherits the global level | + +## Plugins are additive, not overriding + +Plugins stack rather than override. The merger applies all referenced global plugins first (`GlobalWAFPolicy.spec.plugins[]` order), then all namespace plugins (`WAFPolicy.spec.plugins[]` order) — order is array position; there's no priority field. + +This lets baseline rules and application exceptions coexist in one ruleset — the interleaving, and why global `after` runs last, is covered below. + +## Merge order and enforcement + +When a `WAFPolicy` attaches to a target, the controller gathers every applicable policy and plugin, merges them into one ordered list of ModSecurity directives, validates the result, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Each `targetRefs[]` entry produces its own `EnvoyExtensionPolicy` — three routes means three filters. + +Three things govern the outcome: **merge order**, the **admission webhook**, and **validation policies**. + +### The merged ruleset load order + +Directive order isn't cosmetic: the Core Rule Set needs variables set before loading, and post-processing after. Fixed order: + +**Engine setup** +1. `SecRuleEngine On` / `DetectionOnly` — the effective action mode from the merged config. +2. Engine settings — audit logging and request-body access. + +**Plugin `config` (variable setup)** +3. Global plugin `config` blocks, in `GlobalWAFPolicy.spec.plugins[]` order. +4. Namespace plugin `config` blocks, in `WAFPolicy.spec.plugins[]` order. + +**Core Rule Set setup** +5. Core Rule Set setup — reads the `tx.*` variables set above. +6. Blocking paranoia level, from the merged paranoia level. +7. Detection paranoia level (always ≥ blocking level). + +**Pre-Core Rule Set rules (`before`)** +8. Global plugin `before` rules. +9. Namespace plugin `before` rules. + +**Core Rule Set** +10. The full OWASP Core Rule Set. + +**Custom rules (`rules`)** +11. Global plugin `rules`. +12. Namespace plugin `rules`. + +**Post-processing (`after`)** +13. Namespace plugin `after` rules. +14. Global plugin `after` rules — **runs last, giving the cluster operator the final say** on anomaly scoring and logging. + +Pattern: global-before-namespace in the early slots, namespace-before-global in `after`. Within a plugin, its own slots always run `config` → `before` → `rules` → `after`. + +:::note + +When two `WAFPolicy` resources target the same route, $[prodname] follows Gateway API [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins (ties by `namespace/name`). The loser is marked `Accepted=False` reason `Conflicted`, directives not merged. + +::: + +### Structural safety: the admission webhook + +Before any of the above runs, a validating admission webhook checks the raw SecLang in a `WAFPlugin` or `WAFPolicy` at apply time, rejecting the resource if any directive fails one of three checks: + +| Check | Passes when | Rejected with | +| :--- | :--- | :--- | +| Directive is allowed | the directive type is on the allowlist (`SecRule`, `SecAction`, `SecRuleRemoveById`, and other safe directives) | `UnsafeDirective` | +| Rule ID is in range | a `SecRule`/`SecAction` ID falls inside the namespace's allocated ID range | `IDOutsideRange` | +| Removal is permitted | a `SecRuleRemoveById` targets an ID inside the namespace's own range, not a protected baseline rule | `ProtectedIDRemoval` | + +This stops one namespace colliding with another's rule IDs, or disabling the cluster's baseline Core Rule Set rules. Each namespace gets an automatic 10,000-ID range; see [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for how it's derived and which IDs are protected. + +### Validation and enforcement + +The admission webhook checks *raw SecLang*; validation policies check the *merged result*, running after the merge because some problems only show up there. Two innocent plugins can combine to disable all SQL-injection rules — invisible to admission-time checks, but visible to a Rego rule against the merged directive list. + +Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with severity `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: + +| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | +| :--- | :--- | :--- | :--- | +| `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | +| `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | +| `WAFValidationPolicy` | advisory only | surfaces on `WAFPolicy` status; never blocks | surfaces on status; never blocks | -Each resource family — Policy, Plugin, and Validation — comes as a cluster-scoped/namespace-scoped pair. See [How it works](./how-it-works.mdx) for when to reach for each. +To write the rules, see [Rego validation](./reference.mdx#rego-validation). To read the outcome, see [Status conditions](./reference.mdx#status-conditions). ## Next steps -- [How it works](./how-it-works.mdx) — resource families, ownership, merge order, and enforcement. -- [Get started](./get-started.mdx) — enable the feature and block your first attack. -- [Common tasks](./common-tasks.mdx) — set a baseline, attach WAF, write rules, validate config, multi-tenant setup. +- [Enable the feature](./enable.mdx) — turn on Gateway WAF and verify the CRDs and webhook. +- [Get started](./get-started.mdx) — prerequisites and the path to production. +- [Tutorial](./tutorial.mdx) — deploy a test app and block your first attack. +- [Set a baseline](./set-a-baseline.mdx), [Attach WAF](./attach-waf-to-a-route.mdx), [Write custom rules](./write-custom-rules.mdx), [Validate config](./validate-config.mdx), [Multi-tenant setup](./multi-tenant-setup.mdx) — production tasks. - [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. +- [Troubleshooting](./troubleshooting.mdx) — diagnose common failure modes. diff --git a/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx new file mode 100644 index 0000000000..01e0aa5dc8 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx @@ -0,0 +1,71 @@ +--- +description: Run a full multi-tenant Gateway WAF setup — a cluster baseline, org-wide plugin, and validation policy that stops teams weakening protection while each team tunes independently. +--- + +# Multi-tenant setup + +Multi-tenant setup ties the baseline, plugins, and validation guardrails together so a shared cluster stays safe by default while each team still controls its own Gateway. + +:::note[Scenario] + +A cluster shared by `team-a` and `payments`, with a cluster operator setting the baseline via a `GlobalWAFPolicy`, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that stops teams weakening protection. Each team attaches WAF to its own Gateway and tunes independently. + +**Prerequisites:** Gateway WAF enabled; valid license. + +::: + +The cluster operator creates the `default` `GlobalWAFPolicy`, `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` (`Enforce`, scoped to `env: production`) — per the patterns above. Application operators need read access to `Global*` resources to see what they're inheriting: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: waf-global-reader +rules: + - apiGroups: ["applicationlayer.projectcalico.org"] + resources: + - globalwafpolicies + - globalwafplugins + - globalwafvalidationpolicies + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: waf-global-reader + namespace: team-a # repeat for payments +subjects: + - kind: Group + name: team-a-operators + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: waf-global-reader + apiGroup: rbac.authorization.k8s.io +``` + +:::note + +Grants read on cluster-scoped resources via a namespace-scoped `RoleBinding` — application operators can see but not edit `Global*` resources. + +::: + +Each team labels its namespace (`kubectl label namespace team-a env=production`, same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. `team-a` raises paranoia to 2, inheriting `coreRuleSet.state: Enabled`; `payments` runs paranoia 3 plus a namespace `WAFPlugin` blocking GraphQL introspection, using an ID from its own [SecRule ID range](./reference.mdx#secrule-id-ranges). + +## What happens if a team tries to weaken the baseline + +If `payments` applies `action: Detect`, `production-requirements` (`Enforce`) fires its `must-use-block-mode` Critical rule — the controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation: + +```bash +kubectl get wafpolicy payments-waf -n payments \ + -o jsonpath='{.status.validation.failures}' | jq +``` + +No `EnvoyExtensionPolicy` is generated until corrected — restore `action: Block` or request an exception from the cluster operator. + +## Result: isolation in practice + +- **`team-a`** — Core Rule Set at paranoia 2, own custom rules, own ID range. +- **`payments`** — Core Rule Set at paranoia 3, GraphQL introspection block, a different ID range. +- Neither team can disable the org-wide rule from `GlobalWAFPlugin` or drop below Block mode. +- The cluster operator sees all violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — one place to see which namespaces are non-compliant. diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx index d26ec01636..2b0181bad6 100644 --- a/calico-enterprise/threat/gateway-waf/reference.mdx +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -73,7 +73,7 @@ spec: ### Plugin family -`GlobalWAFPlugin`/`WAFPlugin` share an identical spec: custom ModSecurity rules in four lifecycle slots — see [Write custom rules](./common-tasks.mdx#write-custom-rules-with-plugins). +`GlobalWAFPlugin`/`WAFPlugin` share an identical spec: custom ModSecurity rules in four lifecycle slots — see [Write custom rules](./write-custom-rules.mdx). ```yaml apiVersion: applicationlayer.projectcalico.org/v3 @@ -216,7 +216,7 @@ Each rule has a `severity` — `Info`, `Warning`, `Critical` (default `Warning`) - In `Enforce` mode, a `Critical` violation blocks the configuration from being programmed. - In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. -See the enforcement table in [How it works](./how-it-works.mdx#validation-and-enforcement) for the full matrix. +See the enforcement table in [How it works](./index.mdx#validation-and-enforcement) for the full matrix. --- diff --git a/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx new file mode 100644 index 0000000000..091045de25 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx @@ -0,0 +1,93 @@ +--- +description: Set the cluster-wide WAF baseline every namespace inherits — action mode, Core Rule Set state, and paranoia level. +--- + +# Set a cluster-wide baseline + +The baseline is the cluster-wide default every namespace inherits — action mode, Core Rule Set, and paranoia level. Set it once so every Gateway starts protected and teams tune from a known-safe starting point. + +**Prerequisites:** the Gateway WAF feature gate is enabled on `ApplicationLayer` (see [Enable the feature](./enable.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. + +## Create the GlobalWAFPolicy singleton + +Must be named `default` — other names are rejected by the admission webhook. Start in `Detect` mode: it logs matches without blocking, so you can measure false positives before switching to `Block`. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Detect # start here; promote to Block after validating false-positive rate + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +``` + +Apply with `kubectl apply -f globalwafpolicy.yaml`. + +Four paranoia levels; higher catches more attacks at the cost of more false positives: + +| Level | Posture | Typical use | +| :--- | :--- | :--- | +| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | +| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | +| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | +| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | + +:::tip + +Start at level 1 cluster-wide; let application operators raise it per namespace, and enforce a minimum later via a validation policy. + +::: + +Once satisfied, set `defaultAction: Block` — $[prodname] then returns HTTP 403 for matching requests. + +## Add organization-wide rules with a GlobalWAFPlugin + +A `GlobalWAFPlugin` holds custom ModSecurity rules applied cluster-wide via the global policy's `plugins[]` list. Create it, then reference it — rules use the four lifecycle slots in [Write custom rules](./write-custom-rules.mdx). + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPlugin +metadata: + name: org-baseline +spec: + description: "Organization-wide security baseline rules" + config: | + SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | + SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ + "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" + SecRule TX:ENFORCE_HTTPS "@eq 1" "" + rules: | + SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ + "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" + after: | + # Cluster operator's after slot runs last in the merge order. + SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ + "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" +``` + +Reference it from `GlobalWAFPolicy.spec.plugins[]` (`plugins: [{kind: GlobalWAFPlugin, name: org-baseline}]`). `plugins[]` is ordered by array position, up to 64 entries; `WAFPlugin` entries can be added here too. + +## Verify the baseline + +```bash +kubectl get globalwafpolicy default +``` + +``` +NAME ACCEPTED AGE +default True 30s +``` + +Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Accepted` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). + +Every namespace with a `WAFPolicy` inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy` unless it overrides them: + +``` +GlobalWAFPolicy (baseline) + └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 + └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy +``` diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index f640c5937a..f3f0c888a0 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -30,7 +30,7 @@ Raw Envoy proxy logs are a last-resort diagnostic when a hit is missing from the kubectl get wafpolicy -n -o wide ``` -Delete one, or repoint its `targetRefs` — see [How it works](./how-it-works.mdx#merge-order-and-enforcement) for why only one policy owns a target. +Delete one, or repoint its `targetRefs` — see [How it works](./index.mdx#merge-order-and-enforcement) for why only one policy owns a target. --- @@ -100,7 +100,7 @@ kubectl get wafpolicy -n \ `failures[]` names the failed rule, with severity and message; `status.validation.status` is `Rejected`. -**Fix.** Satisfy the failing rule — e.g. restore `action: Block` if the policy requires it — or ask the cluster operator for an exception. See [Validate configuration with Rego](./common-tasks.mdx#validate-configuration-with-rego) and [Status conditions](./reference.mdx#status-conditions). +**Fix.** Satisfy the failing rule — e.g. restore `action: Block` if the policy requires it — or ask the cluster operator for an exception. See [Validate configuration with Rego](./validate-config.mdx) and [Status conditions](./reference.mdx#status-conditions). --- @@ -146,7 +146,7 @@ Also check the `GlobalWAFPolicy` default action, inherited when a namespace `act kubectl get globalwafpolicy default -o jsonpath='{.spec.defaultAction}' ``` -See [How it works](./how-it-works.mdx#inheritance-namespaces-override-they-dont-restart) for field inheritance rules. +See [How it works](./index.mdx#inheritance-namespaces-override-they-dont-restart) for field inheritance rules. --- @@ -167,7 +167,7 @@ kubectl logs -n tigera-gateway \ 1. **Lower the paranoia level**, if false positives come from a broad class of higher-paranoia rules and your threat model allows it. 2. **Add a plugin exception** — suppress via `SecRuleRemoveById`, or skip a known-safe target via `SecRuleUpdateTargetById`. Preferred for surgical tuning without weakening the overall posture. -For how to write exception rules within your namespace's ID range, see [Write custom rules with plugins](./common-tasks.mdx#write-custom-rules-with-plugins). +For how to write exception rules within your namespace's ID range, see [Write custom rules with plugins](./write-custom-rules.mdx). :::caution diff --git a/calico-enterprise/threat/gateway-waf/tutorial.mdx b/calico-enterprise/threat/gateway-waf/tutorial.mdx new file mode 100644 index 0000000000..580a39295c --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/tutorial.mdx @@ -0,0 +1,246 @@ +--- +description: Deploy a test app behind Calico Ingress Gateway, set a WAF baseline, attach it to the Gateway, and confirm a SQL injection and XSS attack are both blocked. +--- + +# Tutorial + +This tutorial takes you from a running Ingress Gateway to a blocked attack in about five minutes. It assumes Gateway WAF is already enabled — see [Enable the feature](./enable.mdx) if you haven't, and [Before you begin](./get-started.mdx#before-you-begin) for the rest of the prerequisites. + +## Step 1: Deploy a test application + +Create an `httpbin` namespace with a deployment and service to act as the backend. + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: httpbin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: httpbin + namespace: httpbin +spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + template: + metadata: + labels: + app: httpbin + spec: + containers: + - name: httpbin + image: kong/httpbin:0.1.0 + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: httpbin + namespace: httpbin +spec: + selector: + app: httpbin + ports: + - protocol: TCP + port: 80 + targetPort: 80 +``` + +Apply it, then wait for the pod to be ready: + +```bash +kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s +``` + +## Step 2: Create a Gateway and HTTPRoute + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: httpbin-gateway + namespace: httpbin +spec: + gatewayClassName: tigera-gateway-class + listeners: + - name: http + protocol: HTTP + port: 9080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin-route + namespace: httpbin +spec: + parentRefs: + - name: httpbin-gateway + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: httpbin + port: 80 +``` + +:::note + +Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod. + +::: + +Apply it, then wait for the Gateway to be accepted: + +```bash +kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s +``` + +## Step 3: Confirm traffic flows + +Find the Envoy service that the Ingress Gateway created for this Gateway. Services are created in the `tigera-gateway` namespace. + +```bash +export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ + -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') +``` + +Send a test request from inside the cluster: + +```bash +kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get +``` + +```text +200 +``` + +Traffic is flowing through the Gateway with no WAF protection yet. + +## Step 4: Apply a GlobalWAFPolicy + +:::tip + +This tutorial uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](./index.mdx#merge-order-and-enforcement) for the tradeoffs. + +::: + +`GlobalWAFPolicy` sets the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFPolicy +metadata: + name: default +spec: + defaultAction: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 1 +``` + +This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. + +## Step 5: Attach WAF to the Gateway + +Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: httpbin-waf + namespace: httpbin +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: httpbin-gateway +``` + +The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. + +Check that the policy was accepted and programmed: + +```bash +kubectl get wafpolicy httpbin-waf -n httpbin \ + -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' +``` + +```text +Licensed=True (LicenseValid) +Accepted=True (Accepted) +Validated=Unknown (NotEvaluated) +Programmed=True (Programmed) +Ready=True (Ready) +``` + +All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. + +If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](./troubleshooting.mdx). + +Allow a few seconds for Envoy to load the WASM filter before running the tests below. + +## Step 6: Test WAF blocking + +**Normal request — expect 200:** + +```bash +kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get +``` + +```text +200 +``` + +**SQL injection probe — expect 403:** + +```bash +kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" +``` + +```text +403 +``` + +**XSS probe — expect 403:** + +```bash +kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" +``` + +```text +403 +``` + +The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. + +## Clean up + +```bash +kubectl delete namespace httpbin +kubectl delete globalwafpolicies default +``` + +## Troubleshoot + +If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reason `WASMUnavailable`, the Coraza WASM image could not be pulled. Private registries require a pull secret to be configured on the `GatewayAPI` CR. See [Troubleshooting](./troubleshooting.mdx) for diagnosis steps and the pull-secret configuration. + +## Next steps + +- [Set a baseline](./set-a-baseline.mdx) — start production configuration in `Detect` mode. +- [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. diff --git a/calico-enterprise/threat/gateway-waf/validate-config.mdx b/calico-enterprise/threat/gateway-waf/validate-config.mdx new file mode 100644 index 0000000000..53806ada49 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/validate-config.mdx @@ -0,0 +1,126 @@ +--- +description: Write Rego validation rules with GlobalWAFValidationPolicy and WAFValidationPolicy to enforce or audit compliance guardrails on the merged WAF configuration. +--- + +# Validate configuration with Rego + +Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang at apply time (structural safety); validation policies check the **merged result** at reconcile time, catching problems only visible once everything's merged. See [Validation and enforcement](./index.mdx#validation-and-enforcement) for the matrix and [Rego validation](./reference.mdx#rego-validation) for the input schema. + +## Cluster operator: GlobalWAFValidationPolicy + +`GlobalWAFValidationPolicy` is cluster-scoped, applying to every namespace or a label-selected subset via `namespaceSelector`. It carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. + +This policy enforces three requirements for namespaces labeled `env: production`, and audits a fourth: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: GlobalWAFValidationPolicy +metadata: + name: production-requirements +spec: + enforcementMode: Enforce + namespaceSelector: + matchLabels: + env: production + rules: + + # Critical: CRS must be enabled + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } + + # Critical: production namespaces must use Block mode + - name: block-mode-required + severity: Critical + rego: | + package waf + violations[{"msg": "WAF must be in Block mode for production"}] { + input.config.action != "Block" + } + + # Critical: minimum paranoia level + - name: min-paranoia-level + severity: Critical + rego: | + package waf + violations[{"msg": msg}] { + input.config.paranoiaLevel < 2 + msg := sprintf( + "paranoia level %d is below the production minimum of 2", + [input.config.paranoiaLevel] + ) + } + + # Warning: advise against disabling RCE rules (does not block) + - name: rce-rules-intact + severity: Warning + rego: | + package waf + violations[{"msg": msg}] { + d := input.directives[_] + startswith(d, "SecRuleRemoveById") + re_match(`93[0-9]{4}`, d) + msg := sprintf("RCE rule removal detected in merged config: %s", [d]) + } +``` + +:::tip + +Start new policies in `Audit` mode; review violations on existing `WAFPolicy` status before switching to `Enforce`, so you don't block teams whose config predates the policy. + +::: + +Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. + +## Application operator: WAFValidationPolicy + +`WAFValidationPolicy` is namespace-scoped and advisory only — violations appear on status but never block programming. Use it as a self-check. + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFValidationPolicy +metadata: + name: team-self-check + namespace: platform +spec: + rules: + + - name: graphql-plugin-active + severity: Warning + rego: | + package waf + violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { + not "graphql-waf" in input.source.namespacePlugins + } +``` + +Being advisory, it's safe to hold yourself to a stricter standard than the baseline — e.g. a higher minimum paranoia level. Violations surface on `WAFPolicy` status without interrupting traffic. + +## Reading violation results + +Violations from matching policies appear in the affected `WAFPolicy`'s status conditions. Check with `kubectl get wafpolicy -n -o yaml`. A blocked policy: + +```yaml +status: + conditions: + - type: Ready + status: "False" + reason: ValidationFailed + message: > + GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": + WAF must be in Block mode for production + - type: Accepted + status: "True" +``` + +An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. + +:::tip + +Write Rego against the schema in [Rego validation](./reference.mdx#rego-validation); test locally with `opa eval` first. A syntax error invalidates the whole policy — status reports which rule failed to parse. + +::: diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx new file mode 100644 index 0000000000..3e21ac85f8 --- /dev/null +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -0,0 +1,130 @@ +--- +description: Write custom ModSecurity rules in a WAFPlugin or GlobalWAFPlugin, using the four lifecycle slots and your namespace's SecRule ID range. +--- + +# Write custom rules with plugins + +A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives in four lifecycle slots, modeled on the Core Rule Set 4 plugin structure — for app-specific detections and false-positive exceptions the Core Rule Set doesn't cover on its own. Cluster operators wanting the same rules org-wide reference `GlobalWAFPlugin` from `GlobalWAFPolicy.spec.plugins[]` instead — same fields, different scope. Slot choice follows [load order](./index.mdx#the-merged-ruleset-load-order): + +| Slot | Runs | Use for | +| :--- | :--- | :--- | +| `config` | Before Core Rule Set setup | `tx.*` variable setup (anomaly thresholds, optional features, app flags). Rules on request variables belong in `before` instead — the request isn't read yet | +| `before` | After variable setup, before Core Rule Set loads | Early-exit allowlisting of trusted paths, request normalization, flags for later rules | +| `rules` | Alongside Core Rule Set, after it (Tigera extension) | App-specific detections that complement rather than pre-empt the Core Rule Set; can act on anomaly scores | +| `after` | Last | False-positive suppression, response inspection, logging. `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, so the cluster operator has final say | + +:::caution + +`ctl:ruleEngine=Off` inside a chained rule or per-request disables the engine for that transaction only. A top-level `SecRuleEngine Off` is rejected by the admission webhook. + +::: + +:::note + +`SecRuleRemoveById`/`ctl:ruleRemoveById` in `after` apply for the rest of phase processing — use to suppress your own false positives. Protected Core Rule Set baseline rules can't be removed from any slot; the webhook rejects the attempt. + +::: + +## Rule IDs and your namespace's range + +Every `SecRule`/`SecAction` needs an ID. Each namespace gets a 10,000-ID block, enforced at admission. Find your range first: + +```bash +kubectl get namespace \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' +# example output: 420000-429999 +``` + +Populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. + +## Worked example + +This `WAFPlugin` uses all four slots to protect a GraphQL API: + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPlugin +metadata: + name: graphql-waf + namespace: platform +spec: + description: "GraphQL introspection guard and false-positive exceptions" + + config: | + # Flag the application type so later rules can branch on it. + SecAction \ + "id:420001,phase:1,pass,nolog,\ + setvar:tx.is_graphql=1" + + before: | + # Allowlist the schema endpoint used by the internal schema registry — + # skip WAF analysis entirely for that specific path. + SecRule REQUEST_URI "@streq /graphql/schema-registry" \ + "id:420010,phase:1,pass,nolog,\ + ctl:ruleEngine=Off,\ + tag:allowlist" + + rules: | + # Block introspection queries (production guard). + SecRule TX:IS_GRAPHQL "@eq 1" \ + "id:420020,phase:2,deny,status:403,\ + msg:'GraphQL introspection blocked',\ + logdata:'body_snippet=%{MATCHED_VAR}',\ + chain" + SecRule REQUEST_BODY "@rx __schema|__type" "" + + # Block suspicious user-agents known to scan GraphQL APIs. + SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ + "id:420021,phase:1,deny,status:403,\ + msg:'Known GraphQL scanner blocked',\ + logdata:'ua=%{MATCHED_VAR}'" + + after: | + # Remove a false positive: CRS rule 941130 fires on a legitimate + # Content-Type value used by our GraphQL client library. + SecRuleRemoveById 941130 +``` + +:::tip + +`@pmf` loads a data file, which must be bundled into the Coraza WASM filter image — ask your cluster operator, or use `@pm` (inline phrase match) for a short string list. + +::: + +A plugin has no effect until a `WAFPolicy` references it, in `spec.plugins[]` (load order = array position): + +```yaml +apiVersion: applicationlayer.projectcalico.org/v3 +kind: WAFPolicy +metadata: + name: platform-api-waf + namespace: platform +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: graphql-api + action: Block + plugins: + - kind: WAFPlugin + name: graphql-waf # ← your plugin above +``` + +If the policy also references a `GlobalWAFPlugin`, its slots load before the namespace plugin's slots at each position. + +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. + +:::note + +Not all SecLang directives are permitted — directives that read the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. + +::: + +Confirm acceptance and reconciliation: + +```bash +kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" +kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" +``` + +`Ready=True` on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed. diff --git a/sidebars-calico-enterprise.js b/sidebars-calico-enterprise.js index f599789080..3d7b9b4f01 100644 --- a/sidebars-calico-enterprise.js +++ b/sidebars-calico-enterprise.js @@ -477,11 +477,16 @@ module.exports = { label: 'Gateway WAF', link: { type: 'doc', id: 'threat/gateway-waf/index' }, items: [ - 'threat/gateway-waf/how-it-works', 'threat/gateway-waf/get-started', - 'threat/gateway-waf/common-tasks', - 'threat/gateway-waf/reference', + 'threat/gateway-waf/tutorial', + 'threat/gateway-waf/enable', + 'threat/gateway-waf/set-a-baseline', + 'threat/gateway-waf/attach-waf-to-a-route', + 'threat/gateway-waf/write-custom-rules', + 'threat/gateway-waf/validate-config', + 'threat/gateway-waf/multi-tenant-setup', 'threat/gateway-waf/troubleshooting', + 'threat/gateway-waf/reference', ], }, ], From a65bb568db08397803ec7beb5b96c94d205a8eb9 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Thu, 9 Jul 2026 18:57:42 +0100 Subject: [PATCH 13/16] docs(gateway-waf): fix Set-a-baseline prerequisite Gateway WAF is gated by the GatewayAPI CR (spec.extensions.waf.state), not the ApplicationLayer resource (that gates the legacy ext_proc WAF). Confirmed against tigera/operator origin/master (IsWAFGatewayExtensionEnabled). Drop the stale ApplicationLayer mention and let the Enable page own the mechanism. Ref: EV-6466 --- calico-enterprise/threat/gateway-waf/set-a-baseline.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx index 091045de25..ff95530576 100644 --- a/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx +++ b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx @@ -6,7 +6,7 @@ description: Set the cluster-wide WAF baseline every namespace inherits — acti The baseline is the cluster-wide default every namespace inherits — action mode, Core Rule Set, and paranoia level. Set it once so every Gateway starts protected and teams tune from a known-safe starting point. -**Prerequisites:** the Gateway WAF feature gate is enabled on `ApplicationLayer` (see [Enable the feature](./enable.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. +**Prerequisites:** Gateway WAF is enabled (see [Enable Gateway WAF](./enable.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. ## Create the GlobalWAFPolicy singleton From 1f47add668da66a50fb194d47ec5ee53131f443f Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Mon, 13 Jul 2026 18:04:04 +0100 Subject: [PATCH 14/16] docs(gateway-waf): apply doc review feedback Restructure the Gateway WAF section per review: - make the category an empty expansion; move its landing content to overview - drop the Get started page; put prerequisites on each task page - add the tech-preview banner to every page - convert procedures to numbered steps (the tutorial is now one list) - rename the baseline page title to name the feature - move the plugin slot table to overview; trim the Rego example on the validation page --- .../threat/deploying-waf-ingress-gateway.mdx | 2 +- .../gateway-waf/attach-waf-to-a-route.mdx | 116 +++-- .../threat/gateway-waf/enable.mdx | 92 ++-- .../threat/gateway-waf/get-started.mdx | 35 -- .../threat/gateway-waf/multi-tenant-setup.mdx | 94 ++-- .../gateway-waf/{index.mdx => overview.mdx} | 12 +- .../threat/gateway-waf/reference.mdx | 8 +- .../threat/gateway-waf/set-a-baseline.mdx | 143 +++--- .../threat/gateway-waf/troubleshooting.mdx | 12 +- .../threat/gateway-waf/tutorial.mdx | 415 +++++++++--------- .../threat/gateway-waf/validate-config.mdx | 207 ++++----- .../threat/gateway-waf/write-custom-rules.mdx | 211 ++++----- sidebars-calico-enterprise.js | 3 +- 13 files changed, 696 insertions(+), 654 deletions(-) delete mode 100644 calico-enterprise/threat/gateway-waf/get-started.mdx rename calico-enterprise/threat/gateway-waf/{index.mdx => overview.mdx} (92%) diff --git a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx index 5f91965f11..ab136c12f2 100644 --- a/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx +++ b/calico-enterprise/threat/deploying-waf-ingress-gateway.mdx @@ -15,7 +15,7 @@ This feature is tech preview. Tech preview features may be subject to significan :::caution -This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/index.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/attach-waf-to-a-route.mdx). This page will be removed in a future release. +This page describes the original, Gateway-wide WAF for Calico Ingress Gateway. It is superseded by [Gateway WAF](./gateway-waf/overview.mdx), which adds per-route and per-namespace policies, custom rules, and compliance validation. For Gateway-wide protection equivalent to this guide, attach a `WAFPolicy` to your `Gateway` — see [Attach WAF to a route](./gateway-waf/attach-waf-to-a-route.mdx). This page will be removed in a future release. ::: diff --git a/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx b/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx index 8ffefbef45..662c70870b 100644 --- a/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx +++ b/calico-enterprise/threat/gateway-waf/attach-waf-to-a-route.mdx @@ -4,9 +4,21 @@ description: Attach a WAFPolicy to a Gateway or HTTPRoute to turn on WAF inspect # Attach WAF to a Gateway or route +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + Create a `WAFPolicy` in your namespace pointing at Gateway API targets — WAF inspection doesn't start automatically when the feature is enabled. It inherits the cluster baseline with namespace overrides layered on top. -**Prerequisites:** the cluster operator has created a `GlobalWAFPolicy`; you have write access to `WAFPolicy` in your namespace; a `Gateway`/`HTTPRoute` is already receiving traffic. +## Prerequisites + +- The cluster operator has created the `GlobalWAFPolicy` baseline. See [Set a cluster-wide baseline for Gateway WAF](./set-a-baseline.mdx). +- You have write access to `WAFPolicy` in your namespace. +- A `Gateway` or `HTTPRoute` in your namespace is already receiving traffic. + +## Choose a target `WAFPolicy.spec.targetRefs[]` accepts two kinds of targets: @@ -23,66 +35,74 @@ Each `targetRefs` entry generates a separate `EnvoyExtensionPolicy` (max 16 entr ::: -Attaching WAF to one `HTTPRoute`, overriding action and paranoia; the unset `coreRuleSet.state` still inherits from the baseline: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: payments-api-waf - namespace: payments -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: payments-api - action: Block # override: Block even if global baseline is Detect - coreRuleSet: - paranoiaLevel: 2 # stricter than the cluster default - # coreRuleSet.state is unset → inherited from GlobalWAFPolicy -``` - -Apply with `kubectl apply -f wafpolicy.yaml`. For custom rules beyond the baseline, see [Write custom rules](./write-custom-rules.mdx); for inheritance details, see the [Overview](./index.mdx#inheritance-namespaces-override-they-dont-restart). +## Attach a WAFPolicy -## Conflict resolution +1. Create a `WAFPolicy` in your namespace pointing at the target. This example attaches to one `HTTPRoute`, overriding action and paranoia; the unset `coreRuleSet.state` still inherits from the baseline: -Only one `WAFPolicy` may be active per target. If two in the same namespace target the same Gateway/HTTPRoute, $[prodname] follows [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins; the newer is marked `Accepted=False` reason `Conflicted`, and only the winner's directives are merged. Fix by deleting the newer policy or repointing its `targetRefs`. + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: WAFPolicy + metadata: + name: payments-api-waf + namespace: payments + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: payments-api + action: Block # override: Block even if global baseline is Detect + coreRuleSet: + paranoiaLevel: 2 # stricter than the cluster default + # coreRuleSet.state is unset → inherited from GlobalWAFPolicy + ``` -## Verify the policy + Apply it with `kubectl apply -f wafpolicy.yaml`. For inheritance details, see the [Overview](./overview.mdx#inheritance-namespaces-override-they-dont-restart). -```bash -kubectl get wafpolicy payments-api-waf -n payments -``` +1. Verify the policy was accepted and programmed: -``` -NAME ACCEPTED PROGRAMMED AGE -payments-api-waf True True 45s -``` + ```bash + kubectl get wafpolicy payments-api-waf -n payments + ``` -`.status.conditions` reports `Accepted`, `Programmed`, and validation findings. Confirm Envoy Gateway received the filter with `kubectl get envoyextensionpolicies -n payments` (one per `targetRefs` entry). + ``` + NAME ACCEPTED PROGRAMMED AGE + payments-api-waf True True 45s + ``` -## Test the WAF + `.status.conditions` reports `Accepted`, `Programmed`, and validation findings. Confirm Envoy Gateway received the filter with `kubectl get envoyextensionpolicies -n payments` (one per `targetRefs` entry). -```bash -curl -si "https:///api?id=1+OR+1%3D1" -``` +1. Test that the WAF blocks an attack: -``` -HTTP/1.1 403 Forbidden -content-length: 11 -content-type: text/plain + ```bash + curl -si "https:///api?id=1+OR+1%3D1" + ``` -Access denied -``` + ``` + HTTP/1.1 403 Forbidden + content-length: 11 + content-type: text/plain -:::tip + Access denied + ``` -In `Detect` mode the request is allowed (200), but the WAF hit is still recorded in the `tigera_secure_ee_waf` log index (carrying `gateway_name`/`gateway_namespace`). On this release, gateway-WAF hits are not raised as Security Events — see [Observing WAF activity](./troubleshooting.mdx#observing-waf-activity). + :::tip -::: + In `Detect` mode the request is allowed (200), but the WAF hit is still recorded in the `tigera_secure_ee_waf` log index (carrying `gateway_name`/`gateway_namespace`). On this release, gateway-WAF hits are not raised as Security Events — see [Observing WAF activity](./troubleshooting.mdx#observing-waf-activity). -:::caution + ::: -`ACCEPTED=False` reason `ValidationFailed` means a `GlobalWAFValidationPolicy` is blocking your config — the status message names the policy and rule. Contact your cluster operator or see [Validate configuration with Rego](./validate-config.mdx). + :::caution -::: + `ACCEPTED=False` reason `ValidationFailed` means a `GlobalWAFValidationPolicy` is blocking your config — the status message names the policy and rule. Contact your cluster operator or see [Validate configuration with Rego](./validate-config.mdx). + + ::: + +## Conflict resolution + +Only one `WAFPolicy` may be active per target. If two in the same namespace target the same Gateway/HTTPRoute, $[prodname] follows [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/): oldest `creationTimestamp` wins; the newer is marked `Accepted=False` reason `Conflicted`, and only the winner's directives are merged. Fix by deleting the newer policy or repointing its `targetRefs`. + +## Next steps + +- [Write custom rules with plugins](./write-custom-rules.mdx) — add app-specific detections beyond the baseline. +- [Validate configuration with Rego](./validate-config.mdx) — check the merged config against compliance guardrails. +- [Troubleshooting](./troubleshooting.mdx) — diagnose a policy that won't accept or program. diff --git a/calico-enterprise/threat/gateway-waf/enable.mdx b/calico-enterprise/threat/gateway-waf/enable.mdx index 66673ea01b..3d47e58bda 100644 --- a/calico-enterprise/threat/gateway-waf/enable.mdx +++ b/calico-enterprise/threat/gateway-waf/enable.mdx @@ -4,62 +4,69 @@ description: Turn Gateway WAF on or off by setting the operator GatewayAPI CR's # Enable Gateway WAF +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + Gateway WAF is off by default. Turning it on is a one-line change to the operator's `GatewayAPI` CR — no new workload is deployed, so there's no extra resource footprint to plan for. -## Enable Gateway WAF +## Prerequisites -Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled`. The default value is `Disabled`. +- $[prodname] is installed with a valid license that includes the Gateway WAF feature. +- $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). +- Cluster-admin rights — the `GatewayAPI` CR is a cluster-scoped operator resource. -```yaml -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled # default is Disabled -``` +:::note -Apply it: +Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires the Ingress Gateway that $[prodname] manages. -```bash -kubectl apply -f - <<'EOF' -apiVersion: operator.tigera.io/v1 -kind: GatewayAPI -metadata: - name: default -spec: - extensions: - waf: - state: Enabled -EOF -``` +::: -The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. +## Enable Gateway WAF -Confirm the six CRDs are installed: +1. Edit the operator `GatewayAPI` CR named `default` and set `spec.extensions.waf.state` to `Enabled` (the default is `Disabled`): -```bash -kubectl get crd | grep applicationlayer.projectcalico.org -``` + ```bash + kubectl apply -f - <<'EOF' + apiVersion: operator.tigera.io/v1 + kind: GatewayAPI + metadata: + name: default + spec: + extensions: + waf: + state: Enabled + EOF + ``` -```text -globalwafplugins.applicationlayer.projectcalico.org -globalwafpolicies.applicationlayer.projectcalico.org -globalwafvalidationpolicies.applicationlayer.projectcalico.org -wafplugins.applicationlayer.projectcalico.org -wafpolicies.applicationlayer.projectcalico.org -wafvalidationpolicies.applicationlayer.projectcalico.org -``` + The operator installs the six WAF CRDs and the validating admission webhook, and wires the WAF reconcilers into the existing `calico-kube-controllers` pod in `calico-system`. No new workload is deployed. -All six CRDs must be present before you create any WAF resources. +1. Confirm the six CRDs are installed: -:::tip + ```bash + kubectl get crd | grep applicationlayer.projectcalico.org + ``` -To confirm the admission webhook is active, run `kubectl get validatingwebhookconfigurations | grep waf`. You should see one entry. If it is absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. + ```text + globalwafplugins.applicationlayer.projectcalico.org + globalwafpolicies.applicationlayer.projectcalico.org + globalwafvalidationpolicies.applicationlayer.projectcalico.org + wafplugins.applicationlayer.projectcalico.org + wafpolicies.applicationlayer.projectcalico.org + wafvalidationpolicies.applicationlayer.projectcalico.org + ``` -::: + All six must be present before you create any WAF resources. + +1. Confirm the admission webhook is active: + + ```bash + kubectl get validatingwebhookconfigurations | grep waf + ``` + + You should see one entry. If it's absent, check the `calico-kube-controllers` pod logs in the `calico-system` namespace. ## Disable Gateway WAF @@ -81,3 +88,4 @@ The operator removes the admission webhook and stops the WAF reconcilers; existi ## Next steps - [Tutorial](./tutorial.mdx) — deploy a test app and block your first attack. +- [Set a cluster-wide baseline for Gateway WAF](./set-a-baseline.mdx) — start your production rollout. diff --git a/calico-enterprise/threat/gateway-waf/get-started.mdx b/calico-enterprise/threat/gateway-waf/get-started.mdx deleted file mode 100644 index 8011762beb..0000000000 --- a/calico-enterprise/threat/gateway-waf/get-started.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -description: Prerequisites for Gateway WAF, and the path from enabling the feature through the tutorial to production rollout. ---- - -# Get started - -:::note - -This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. - -::: - -Gateway WAF protects traffic through $[prodname] Ingress Gateway with a per-route, per-namespace web application firewall: a cluster operator sets a baseline once, and application operators layer their own rules on top. This page gets you oriented — what you need before you start, and where to go next. - -## Before you begin - -**Prerequisites:** - -- $[prodname] is installed with a valid license that includes the Gateway WAF feature. -- $[prodname] Ingress Gateway is already set up. See [About Calico Ingress Gateway](../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). -- You have a namespace where you can deploy a test application (needed for the tutorial). - -:::note - -Customer-installed (BYO) Envoy Gateway is not supported. Gateway WAF requires the Ingress Gateway that $[prodname] manages. - -::: - -## The path - -1. [Enable the feature](./enable.mdx) — turn on Gateway WAF on the operator `GatewayAPI` CR. -2. [Try the tutorial](./tutorial.mdx) — deploy a test app and block a SQL injection and XSS attack in about five minutes. -3. [Move to production](./set-a-baseline.mdx) — set a cluster-wide baseline, then [attach WAF](./attach-waf-to-a-route.mdx), [write custom rules](./write-custom-rules.mdx), [validate configuration](./validate-config.mdx), and set up [multi-tenant](./multi-tenant-setup.mdx) ownership. - -For concepts — resource families, merge order, and enforcement — see the [Overview](./index.mdx). If something isn't working as expected, [Troubleshooting](./troubleshooting.mdx) walks through common failure modes. diff --git a/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx index 01e0aa5dc8..6213022289 100644 --- a/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx +++ b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx @@ -4,53 +4,66 @@ description: Run a full multi-tenant Gateway WAF setup — a cluster baseline, o # Multi-tenant setup -Multi-tenant setup ties the baseline, plugins, and validation guardrails together so a shared cluster stays safe by default while each team still controls its own Gateway. - -:::note[Scenario] - -A cluster shared by `team-a` and `payments`, with a cluster operator setting the baseline via a `GlobalWAFPolicy`, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that stops teams weakening protection. Each team attaches WAF to its own Gateway and tunes independently. +:::note -**Prerequisites:** Gateway WAF enabled; valid license. +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. ::: -The cluster operator creates the `default` `GlobalWAFPolicy`, `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` (`Enforce`, scoped to `env: production`) — per the patterns above. Application operators need read access to `Global*` resources to see what they're inheriting: - -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: waf-global-reader -rules: - - apiGroups: ["applicationlayer.projectcalico.org"] - resources: - - globalwafpolicies - - globalwafplugins - - globalwafvalidationpolicies - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: waf-global-reader - namespace: team-a # repeat for payments -subjects: - - kind: Group - name: team-a-operators - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: ClusterRole - name: waf-global-reader - apiGroup: rbac.authorization.k8s.io -``` +Multi-tenant setup ties the baseline, plugins, and validation guardrails together so a shared cluster stays safe by default while each team still controls its own Gateway. -:::note +:::note[Scenario] -Grants read on cluster-scoped resources via a namespace-scoped `RoleBinding` — application operators can see but not edit `Global*` resources. +A cluster shared by `team-a` and `payments`, with a cluster operator setting the baseline via a `GlobalWAFPolicy`, an org-wide `GlobalWAFPlugin`, and a `GlobalWAFValidationPolicy` that stops teams weakening protection. Each team attaches WAF to its own Gateway and tunes independently. ::: -Each team labels its namespace (`kubectl label namespace team-a env=production`, same for `payments`) so the validation policy applies, then creates a `WAFPolicy` targeting its own Gateway. `team-a` raises paranoia to 2, inheriting `coreRuleSet.state: Enabled`; `payments` runs paranoia 3 plus a namespace `WAFPlugin` blocking GraphQL introspection, using an ID from its own [SecRule ID range](./reference.mdx#secrule-id-ranges). +## Prerequisites + +- Gateway WAF is enabled and licensed. See [Enable Gateway WAF](./enable.mdx). +- Cluster-admin rights for the cluster-operator steps; each team needs write access to WAF resources in its own namespace. + +## Set it up + +1. As cluster operator, create the baseline resources: the `default` `GlobalWAFPolicy`, an `org-security-baseline` `GlobalWAFPlugin`, and a `production-requirements` `GlobalWAFValidationPolicy` (`Enforce`, scoped to `env: production`). Follow the patterns in [Set a cluster-wide baseline](./set-a-baseline.mdx), [Write custom rules](./write-custom-rules.mdx), and [Validate configuration with Rego](./validate-config.mdx). + +1. Grant application operators read access to the `Global*` resources so they can see what they're inheriting: + + ```yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: waf-global-reader + rules: + - apiGroups: ["applicationlayer.projectcalico.org"] + resources: + - globalwafpolicies + - globalwafplugins + - globalwafvalidationpolicies + verbs: ["get", "list", "watch"] + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: waf-global-reader + namespace: team-a # repeat for payments + subjects: + - kind: Group + name: team-a-operators + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: waf-global-reader + apiGroup: rbac.authorization.k8s.io + ``` + + :::note + + This grants read on cluster-scoped resources via a namespace-scoped `RoleBinding` — application operators can see but not edit `Global*` resources. + + ::: + +1. Each team labels its namespace so the validation policy applies (`kubectl label namespace team-a env=production`, same for `payments`), then creates a `WAFPolicy` targeting its own Gateway. `team-a` raises paranoia to 2, inheriting `coreRuleSet.state: Enabled`; `payments` runs paranoia 3 plus a namespace `WAFPlugin` blocking GraphQL introspection, using an ID from its own [SecRule ID range](./reference.mdx#secrule-id-ranges). ## What happens if a team tries to weaken the baseline @@ -69,3 +82,8 @@ No `EnvoyExtensionPolicy` is generated until corrected — restore `action: Bloc - **`payments`** — Core Rule Set at paranoia 3, GraphQL introspection block, a different ID range. - Neither team can disable the org-wide rule from `GlobalWAFPlugin` or drop below Block mode. - The cluster operator sees all violations centrally on `GlobalWAFValidationPolicy/production-requirements` status — one place to see which namespaces are non-compliant. + +## Next steps + +- [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. +- [Troubleshooting](./troubleshooting.mdx) — diagnose a policy blocked by a guardrail. diff --git a/calico-enterprise/threat/gateway-waf/index.mdx b/calico-enterprise/threat/gateway-waf/overview.mdx similarity index 92% rename from calico-enterprise/threat/gateway-waf/index.mdx rename to calico-enterprise/threat/gateway-waf/overview.mdx index 93d77ae881..335a76bf4d 100644 --- a/calico-enterprise/threat/gateway-waf/index.mdx +++ b/calico-enterprise/threat/gateway-waf/overview.mdx @@ -59,6 +59,17 @@ A **plugin** holds custom ModSecurity rules in four lifecycle slots — `config` - `GlobalWAFPlugin` — organization-wide rules: security baselines, compliance rules. - `WAFPlugin` — application-specific rules and false-positive exceptions. +Each slot runs at a fixed point in the merge, so slot choice decides when your rules fire: + +| Slot | Runs | Use for | +| :--- | :--- | :--- | +| `config` | Before Core Rule Set setup | `tx.*` variable setup (anomaly thresholds, optional features, app flags). Rules on request variables belong in `before` instead — the request isn't read yet | +| `before` | After variable setup, before Core Rule Set loads | Early-exit allowlisting of trusted paths, request normalization, flags for later rules | +| `rules` | Alongside Core Rule Set, after it (Tigera extension) | App-specific detections that complement rather than pre-empt the Core Rule Set; can act on anomaly scores | +| `after` | Last | False-positive suppression, response inspection, logging. `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, so the cluster operator has final say | + +For the full merge sequence across every policy and plugin, see [merged ruleset load order](#the-merged-ruleset-load-order); to write plugin rules, see [Write custom rules](./write-custom-rules.mdx). + ## Validation policies — guardrails A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated against the *merged, effective* configuration — answering "is this acceptable?", e.g. "Core Rule Set must stay enabled" or "production must run in Block mode." @@ -182,7 +193,6 @@ To write the rules, see [Rego validation](./reference.mdx#rego-validation). To r ## Next steps - [Enable the feature](./enable.mdx) — turn on Gateway WAF and verify the CRDs and webhook. -- [Get started](./get-started.mdx) — prerequisites and the path to production. - [Tutorial](./tutorial.mdx) — deploy a test app and block your first attack. - [Set a baseline](./set-a-baseline.mdx), [Attach WAF](./attach-waf-to-a-route.mdx), [Write custom rules](./write-custom-rules.mdx), [Validate config](./validate-config.mdx), [Multi-tenant setup](./multi-tenant-setup.mdx) — production tasks. - [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx index 2b0181bad6..706ecb1882 100644 --- a/calico-enterprise/threat/gateway-waf/reference.mdx +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -4,6 +4,12 @@ description: Field reference for the six Gateway WAF custom resources, the Rego # Reference +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + ## CRDs Six custom resources in the `applicationlayer.projectcalico.org/v3` API group, in three families; each has a cluster-scoped and namespace-scoped resource sharing almost all fields. @@ -216,7 +222,7 @@ Each rule has a `severity` — `Info`, `Warning`, `Critical` (default `Warning`) - In `Enforce` mode, a `Critical` violation blocks the configuration from being programmed. - In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. -See the enforcement table in [How it works](./index.mdx#validation-and-enforcement) for the full matrix. +See the enforcement table in [How it works](./overview.mdx#validation-and-enforcement) for the full matrix. --- diff --git a/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx index ff95530576..986410a075 100644 --- a/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx +++ b/calico-enterprise/threat/gateway-waf/set-a-baseline.mdx @@ -2,87 +2,100 @@ description: Set the cluster-wide WAF baseline every namespace inherits — action mode, Core Rule Set state, and paranoia level. --- -# Set a cluster-wide baseline +# Set a cluster-wide baseline for Gateway WAF + +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: The baseline is the cluster-wide default every namespace inherits — action mode, Core Rule Set, and paranoia level. Set it once so every Gateway starts protected and teams tune from a known-safe starting point. -**Prerequisites:** Gateway WAF is enabled (see [Enable Gateway WAF](./enable.mdx)); cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. +## Prerequisites -## Create the GlobalWAFPolicy singleton +- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- Cluster-admin rights — `GlobalWAFPolicy` is cluster-scoped. -Must be named `default` — other names are rejected by the admission webhook. Start in `Detect` mode: it logs matches without blocking, so you can measure false positives before switching to `Block`. +## Set the baseline -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Detect # start here; promote to Block after validating false-positive rate - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` +1. Create the singleton `GlobalWAFPolicy`. It must be named `default` — other names are rejected by the admission webhook. Start in `Detect` mode: it logs matches without blocking, so you can measure false positives before switching to `Block`: -Apply with `kubectl apply -f globalwafpolicy.yaml`. + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: GlobalWAFPolicy + metadata: + name: default + spec: + defaultAction: Detect # start here; promote to Block after validating false-positive rate + coreRuleSet: + state: Enabled + paranoiaLevel: 1 + ``` -Four paranoia levels; higher catches more attacks at the cost of more false positives: + Apply it with `kubectl apply -f globalwafpolicy.yaml`. -| Level | Posture | Typical use | -| :--- | :--- | :--- | -| 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | -| 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | -| 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | -| 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | +1. Pick a paranoia level. Higher catches more attacks at the cost of more false positives: -:::tip + | Level | Posture | Typical use | + | :--- | :--- | :--- | + | 1 | Balanced — low false-positive rate | General-purpose default; a safe starting point | + | 2 | Moderate — catches common evasion patterns | APIs that handle sensitive data | + | 3 | Strict — flags most suspicious patterns | High-value or compliance-regulated endpoints | + | 4 | Paranoid — near-zero tolerance | Extremely high-security targets; expect false positives | -Start at level 1 cluster-wide; let application operators raise it per namespace, and enforce a minimum later via a validation policy. + :::tip -::: + Start at level 1 cluster-wide; let application operators raise it per namespace, and enforce a minimum later via a validation policy. -Once satisfied, set `defaultAction: Block` — $[prodname] then returns HTTP 403 for matching requests. - -## Add organization-wide rules with a GlobalWAFPlugin - -A `GlobalWAFPlugin` holds custom ModSecurity rules applied cluster-wide via the global policy's `plugins[]` list. Create it, then reference it — rules use the four lifecycle slots in [Write custom rules](./write-custom-rules.mdx). - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPlugin -metadata: - name: org-baseline -spec: - description: "Organization-wide security baseline rules" - config: | - SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" - before: | - SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ - "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" - SecRule TX:ENFORCE_HTTPS "@eq 1" "" - rules: | - SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ - "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" - after: | - # Cluster operator's after slot runs last in the merge order. - SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ - "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" -``` + ::: + +1. Once you've validated the false-positive rate, set `defaultAction: Block`. $[prodname] then returns HTTP 403 for matching requests. + +## Add organization-wide rules with a GlobalWAFPlugin (optional) -Reference it from `GlobalWAFPolicy.spec.plugins[]` (`plugins: [{kind: GlobalWAFPlugin, name: org-baseline}]`). `plugins[]` is ordered by array position, up to 64 entries; `WAFPlugin` entries can be added here too. +A `GlobalWAFPlugin` holds custom ModSecurity rules applied cluster-wide via the global policy's `plugins[]` list — for a security baseline every namespace inherits. + +1. Create the `GlobalWAFPlugin`. Rules use the four lifecycle slots described in [Write custom rules](./write-custom-rules.mdx): + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: GlobalWAFPlugin + metadata: + name: org-baseline + spec: + description: "Organization-wide security baseline rules" + config: | + SecAction "id:800001,phase:1,pass,nolog,setvar:tx.enforce_https=1" + before: | + SecRule REQUEST_HEADERS:X-Forwarded-Proto "!@streq https" \ + "id:800002,phase:1,deny,status:403,msg:'HTTPS required',chain" + SecRule TX:ENFORCE_HTTPS "@eq 1" "" + rules: | + SecRule REQUEST_HEADERS:X-Request-ID "@rx ^$" \ + "id:800003,phase:1,deny,status:400,msg:'Missing X-Request-ID header'" + after: | + # Cluster operator's after slot runs last in the merge order. + SecRule TX:BLOCKING_ANOMALY_SCORE "@gt 0" \ + "id:800099,phase:5,pass,log,msg:'Request blocked by WAF'" + ``` + +1. Reference it from `GlobalWAFPolicy.spec.plugins[]` — for example `plugins: [{kind: GlobalWAFPlugin, name: org-baseline}]`. `plugins[]` is ordered by array position, up to 64 entries; `WAFPlugin` entries can be added here too. ## Verify the baseline -```bash -kubectl get globalwafpolicy default -``` +1. Confirm the policy is accepted: -``` -NAME ACCEPTED AGE -default True 30s -``` + ```bash + kubectl get globalwafpolicy default + ``` + + ``` + NAME ACCEPTED AGE + default True 30s + ``` -Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Accepted` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (message describes the violation). See [Status conditions](./reference.mdx#status-conditions). +Detail lives in `.status.conditions` (`kubectl get globalwafpolicy default -o yaml`): `Accepted=True` reason `Accepted` = programmed; `Accepted=False` reason `ValidationFailed` = a validation policy blocked it (the message describes the violation). See [Status conditions](./reference.mdx#status-conditions). Every namespace with a `WAFPolicy` inherits `defaultAction`, `coreRuleSet.state`, and `coreRuleSet.paranoiaLevel` from `GlobalWAFPolicy` unless it overrides them: @@ -91,3 +104,9 @@ GlobalWAFPolicy (baseline) └─ WAFPolicy in namespace payments → overrides action: Block, paranoiaLevel: 2 └─ WAFPolicy in namespace team-a → inherits everything from GlobalWAFPolicy ``` + +## Next steps + +- [Attach WAF to a Gateway or route](./attach-waf-to-a-route.mdx) — turn on inspection for a target. +- [Write custom rules with plugins](./write-custom-rules.mdx) — add app-specific detections and exceptions. +- [Validate configuration with Rego](./validate-config.mdx) — enforce compliance guardrails on the merged config. diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index f3f0c888a0..63596f7d04 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -4,6 +4,12 @@ description: Diagnose and fix common Gateway WAF problems by reading policy stat # Troubleshooting +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: + Most problems surface on the policy's status conditions: ```bash @@ -30,7 +36,7 @@ Raw Envoy proxy logs are a last-resort diagnostic when a hit is missing from the kubectl get wafpolicy -n -o wide ``` -Delete one, or repoint its `targetRefs` — see [How it works](./index.mdx#merge-order-and-enforcement) for why only one policy owns a target. +Delete one, or repoint its `targetRefs` — see [How it works](./overview.mdx#merge-order-and-enforcement) for why only one policy owns a target. --- @@ -146,7 +152,7 @@ Also check the `GlobalWAFPolicy` default action, inherited when a namespace `act kubectl get globalwafpolicy default -o jsonpath='{.spec.defaultAction}' ``` -See [How it works](./index.mdx#inheritance-namespaces-override-they-dont-restart) for field inheritance rules. +See [How it works](./overview.mdx#inheritance-namespaces-override-they-dont-restart) for field inheritance rules. --- @@ -186,7 +192,7 @@ kubectl get wafpolicy -n \ -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}): {.message}{"\n"}{end}' ``` -1. `Licensed=False` → license missing or doesn't entitle Gateway WAF. Check [Get started](./get-started.mdx). +1. `Licensed=False` → license missing or doesn't entitle Gateway WAF. Check the [Enable Gateway WAF](./enable.mdx#prerequisites) prerequisites. 2. `Accepted=False` → spec rejected. Read the `Reason`/`message` — see the sections above. 3. `Validated=False` → a validation rule fired. Read `status.validation.failures`. 4. `Programmed=False` → filter not loaded into Envoy. Read the `Reason` — see `WASMUnavailable`/`NotAttempted` above. diff --git a/calico-enterprise/threat/gateway-waf/tutorial.mdx b/calico-enterprise/threat/gateway-waf/tutorial.mdx index 580a39295c..bc242fe5cc 100644 --- a/calico-enterprise/threat/gateway-waf/tutorial.mdx +++ b/calico-enterprise/threat/gateway-waf/tutorial.mdx @@ -4,230 +4,211 @@ description: Deploy a test app behind Calico Ingress Gateway, set a WAF baseline # Tutorial -This tutorial takes you from a running Ingress Gateway to a blocked attack in about five minutes. It assumes Gateway WAF is already enabled — see [Enable the feature](./enable.mdx) if you haven't, and [Before you begin](./get-started.mdx#before-you-begin) for the rest of the prerequisites. - -## Step 1: Deploy a test application - -Create an `httpbin` namespace with a deployment and service to act as the backend. - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: httpbin ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: httpbin - namespace: httpbin -spec: - replicas: 1 - selector: - matchLabels: - app: httpbin - template: - metadata: - labels: - app: httpbin - spec: - containers: - - name: httpbin - image: kong/httpbin:0.1.0 - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: httpbin - namespace: httpbin -spec: - selector: - app: httpbin - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Apply it, then wait for the pod to be ready: - -```bash -kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s -``` - -## Step 2: Create a Gateway and HTTPRoute - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: httpbin-gateway - namespace: httpbin -spec: - gatewayClassName: tigera-gateway-class - listeners: - - name: http - protocol: HTTP - port: 9080 ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: httpbin-route - namespace: httpbin -spec: - parentRefs: - - name: httpbin-gateway - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: httpbin - port: 80 -``` - :::note -Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod. - -::: - -Apply it, then wait for the Gateway to be accepted: - -```bash -kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s -``` - -## Step 3: Confirm traffic flows - -Find the Envoy service that the Ingress Gateway created for this Gateway. Services are created in the `tigera-gateway` namespace. - -```bash -export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ - -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') -``` - -Send a test request from inside the cluster: - -```bash -kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get -``` - -```text -200 -``` - -Traffic is flowing through the Gateway with no WAF protection yet. - -## Step 4: Apply a GlobalWAFPolicy - -:::tip - -This tutorial uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](./index.mdx#merge-order-and-enforcement) for the tradeoffs. +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. ::: -`GlobalWAFPolicy` sets the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFPolicy -metadata: - name: default -spec: - defaultAction: Block - coreRuleSet: - state: Enabled - paranoiaLevel: 1 -``` - -This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. - -## Step 5: Attach WAF to the Gateway - -Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway. - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: httpbin-waf - namespace: httpbin -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: httpbin-gateway -``` - -The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. - -Check that the policy was accepted and programmed: - -```bash -kubectl get wafpolicy httpbin-waf -n httpbin \ - -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' -``` - -```text -Licensed=True (LicenseValid) -Accepted=True (Accepted) -Validated=Unknown (NotEvaluated) -Programmed=True (Programmed) -Ready=True (Ready) -``` - -All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. - -If `Programmed=False`, check the condition message for the reason. A common cause is the WASM image not being reachable — see [Troubleshooting](./troubleshooting.mdx). - -Allow a few seconds for Envoy to load the WASM filter before running the tests below. +This tutorial takes you from a running Ingress Gateway to a blocked attack in about five minutes. + +## Prerequisites + +- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- $[prodname] Ingress Gateway is set up. See [About Calico Ingress Gateway](../../networking/ingress-gateway/about-calico-ingress-gateway.mdx). +- A namespace where you can deploy a test app — this tutorial creates one called `httpbin`. + +## Block an attack in five minutes + +1. Deploy a test application. Create an `httpbin` namespace with a deployment and service to act as the backend: + + ```yaml + apiVersion: v1 + kind: Namespace + metadata: + name: httpbin + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: httpbin + namespace: httpbin + spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + template: + metadata: + labels: + app: httpbin + spec: + containers: + - name: httpbin + image: kong/httpbin:0.1.0 + ports: + - containerPort: 80 + --- + apiVersion: v1 + kind: Service + metadata: + name: httpbin + namespace: httpbin + spec: + selector: + app: httpbin + ports: + - protocol: TCP + port: 80 + targetPort: 80 + ``` + + Apply it, then wait for the pod to be ready: + + ```bash + kubectl wait --for=condition=Ready pod -l app=httpbin -n httpbin --timeout=60s + ``` + +1. Create a Gateway and HTTPRoute: + + ```yaml + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: httpbin-gateway + namespace: httpbin + spec: + gatewayClassName: tigera-gateway-class + listeners: + - name: http + protocol: HTTP + port: 9080 + --- + apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httpbin-route + namespace: httpbin + spec: + parentRefs: + - name: httpbin-gateway + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: httpbin + port: 80 + ``` + + :::note + + Use port 9080, not 8080. Port 8080 is reserved by the Calico L7 log collector inside the Envoy proxy pod. + + ::: + + Apply it, then wait for the Gateway to be accepted: + + ```bash + kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s + ``` + +1. Confirm traffic flows. Find the Envoy service the Ingress Gateway created for this Gateway (services are created in the `tigera-gateway` namespace): + + ```bash + export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ + -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') + ``` + + Send a test request from inside the cluster: + + ```bash + kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get + ``` + + ```text + 200 + ``` + + Traffic is flowing through the Gateway with no WAF protection yet. + +1. Apply a `GlobalWAFPolicy` to set the cluster-wide baseline. Create the singleton `default` policy with the OWASP Core Rule Set enabled in `Block` mode: + + :::tip + + This tutorial uses `Block` mode immediately so you can see blocking in action. Most production rollouts start in `Detect` mode to baseline traffic before switching. See [Merge order and enforcement](./overview.mdx#merge-order-and-enforcement) for the tradeoffs. + + ::: + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: GlobalWAFPolicy + metadata: + name: default + spec: + defaultAction: Block + coreRuleSet: + state: Enabled + paranoiaLevel: 1 + ``` + + This defines the global posture. It does not attach WAF to any traffic yet — that requires a namespace-scoped `WAFPolicy` with `targetRefs`. + +1. Attach WAF to the Gateway. Create a `WAFPolicy` in the `httpbin` namespace targeting the Gateway: + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: WAFPolicy + metadata: + name: httpbin-waf + namespace: httpbin + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: httpbin-gateway + ``` + + The reconciler merges the `GlobalWAFPolicy` baseline with this `WAFPolicy`, generates an ordered ModSecurity ruleset, and programs it into Envoy Gateway as an `EnvoyExtensionPolicy`. Check that the policy was accepted and programmed: + + ```bash + kubectl get wafpolicy httpbin-waf -n httpbin \ + -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}' + ``` + + ```text + Licensed=True (LicenseValid) + Accepted=True (Accepted) + Validated=Unknown (NotEvaluated) + Programmed=True (Programmed) + Ready=True (Ready) + ``` + + All four executable conditions (`Licensed`, `Accepted`, `Programmed`, `Ready`) must be `True` before WAF is active. `Validated=Unknown (NotEvaluated)` is expected when no `WAFValidationPolicy` is in scope. If `Programmed=False`, check the condition message — a common cause is the WASM image not being reachable (see [Troubleshooting](./troubleshooting.mdx)). Allow a few seconds for Envoy to load the WASM filter before running the tests below. + +1. Test WAF blocking. A normal request still passes, but a SQL injection probe and an XSS probe are both blocked: + + ```bash + # Normal request — expect 200 + kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get -## Step 6: Test WAF blocking + # SQL injection probe — expect 403 + kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" -**Normal request — expect 200:** - -```bash -kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get -``` - -```text -200 -``` - -**SQL injection probe — expect 403:** - -```bash -kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" -``` - -```text -403 -``` - -**XSS probe — expect 403:** - -```bash -kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ - curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" -``` - -```text -403 -``` + # XSS probe — expect 403 + kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ + curl -s -o /dev/null -w "%{http_code}" \ + "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" + ``` -The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. + The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. ## Clean up @@ -242,5 +223,5 @@ If a `WAFPolicy` you attach to a route later shows `Programmed=False` with reaso ## Next steps -- [Set a baseline](./set-a-baseline.mdx) — start production configuration in `Detect` mode. +- [Set a cluster-wide baseline for Gateway WAF](./set-a-baseline.mdx) — start production configuration in `Detect` mode. - [Reference](./reference.mdx) — CRDs, Rego validation, SecRule ID ranges, and status conditions. diff --git a/calico-enterprise/threat/gateway-waf/validate-config.mdx b/calico-enterprise/threat/gateway-waf/validate-config.mdx index 53806ada49..54f9ebed4e 100644 --- a/calico-enterprise/threat/gateway-waf/validate-config.mdx +++ b/calico-enterprise/threat/gateway-waf/validate-config.mdx @@ -4,123 +4,124 @@ description: Write Rego validation rules with GlobalWAFValidationPolicy and WAFV # Validate configuration with Rego -Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang at apply time (structural safety); validation policies check the **merged result** at reconcile time, catching problems only visible once everything's merged. See [Validation and enforcement](./index.mdx#validation-and-enforcement) for the matrix and [Rego validation](./reference.mdx#rego-validation) for the input schema. +:::note -## Cluster operator: GlobalWAFValidationPolicy +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. -`GlobalWAFValidationPolicy` is cluster-scoped, applying to every namespace or a label-selected subset via `namespaceSelector`. It carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. +::: -This policy enforces three requirements for namespaces labeled `env: production`, and audits a fourth: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: GlobalWAFValidationPolicy -metadata: - name: production-requirements -spec: - enforcementMode: Enforce - namespaceSelector: - matchLabels: - env: production - rules: - - # Critical: CRS must be enabled - - name: crs-must-be-enabled - severity: Critical - rego: | - package waf - violations[{"msg": "OWASP CRS must be enabled"}] { - input.config.crsState != "Enabled" - } - - # Critical: production namespaces must use Block mode - - name: block-mode-required - severity: Critical - rego: | - package waf - violations[{"msg": "WAF must be in Block mode for production"}] { - input.config.action != "Block" - } - - # Critical: minimum paranoia level - - name: min-paranoia-level - severity: Critical - rego: | - package waf - violations[{"msg": msg}] { - input.config.paranoiaLevel < 2 - msg := sprintf( - "paranoia level %d is below the production minimum of 2", - [input.config.paranoiaLevel] - ) - } - - # Warning: advise against disabling RCE rules (does not block) - - name: rce-rules-intact - severity: Warning - rego: | - package waf - violations[{"msg": msg}] { - d := input.directives[_] - startswith(d, "SecRuleRemoveById") - re_match(`93[0-9]{4}`, d) - msg := sprintf("RCE rule removal detected in merged config: %s", [d]) - } -``` +Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang at apply time (structural safety); validation policies check the **merged result** at reconcile time, catching problems only visible once everything's merged. See [Validation and enforcement](./overview.mdx#validation-and-enforcement) for the matrix and [Rego validation](./reference.mdx#rego-validation) for the input schema. -:::tip +## Prerequisites -Start new policies in `Audit` mode; review violations on existing `WAFPolicy` status before switching to `Enforce`, so you don't block teams whose config predates the policy. +- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- Cluster-admin rights to create a `GlobalWAFValidationPolicy` (it's cluster-scoped). A `WAFValidationPolicy` only needs namespace write access. +- Rego rules evaluate against the merged config — see [Rego validation](./reference.mdx#rego-validation) for the input schema and worked examples. -::: +## Enforce guardrails cluster-wide with GlobalWAFValidationPolicy -Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. +`GlobalWAFValidationPolicy` is cluster-scoped, applying to every namespace or a label-selected subset via `namespaceSelector`. It carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. -## Application operator: WAFValidationPolicy +1. Write the policy with Rego rules and an `enforcementMode`. This one requires the Core Rule Set enabled and `Block` mode for namespaces labeled `env: production`: + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: GlobalWAFValidationPolicy + metadata: + name: production-requirements + spec: + enforcementMode: Enforce + namespaceSelector: + matchLabels: + env: production + rules: + + # Critical: CRS must be enabled + - name: crs-must-be-enabled + severity: Critical + rego: | + package waf + violations[{"msg": "OWASP CRS must be enabled"}] { + input.config.crsState != "Enabled" + } + + # Critical: production namespaces must use Block mode + - name: block-mode-required + severity: Critical + rego: | + package waf + violations[{"msg": "WAF must be in Block mode for production"}] { + input.config.action != "Block" + } + ``` + + For more rule patterns — a minimum paranoia level, or inspecting the merged directive list to catch a plugin removing a baseline rule — see [Rego validation](./reference.mdx#rego-validation). + +1. Start in `Audit` mode, review violations on existing `WAFPolicy` status, then switch to `Enforce` — so you don't block teams whose config predates the policy. + + Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. + +## Self-check your config with WAFValidationPolicy `WAFValidationPolicy` is namespace-scoped and advisory only — violations appear on status but never block programming. Use it as a self-check. -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFValidationPolicy -metadata: - name: team-self-check - namespace: platform -spec: - rules: - - - name: graphql-plugin-active - severity: Warning - rego: | - package waf - violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { - not "graphql-waf" in input.source.namespacePlugins - } -``` - -Being advisory, it's safe to hold yourself to a stricter standard than the baseline — e.g. a higher minimum paranoia level. Violations surface on `WAFPolicy` status without interrupting traffic. - -## Reading violation results - -Violations from matching policies appear in the affected `WAFPolicy`'s status conditions. Check with `kubectl get wafpolicy -n -o yaml`. A blocked policy: - -```yaml -status: - conditions: - - type: Ready - status: "False" - reason: ValidationFailed - message: > - GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": - WAF must be in Block mode for production - - type: Accepted - status: "True" -``` - -An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. +1. Write a `WAFValidationPolicy` in your namespace: + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: WAFValidationPolicy + metadata: + name: team-self-check + namespace: platform + spec: + rules: + + - name: graphql-plugin-active + severity: Warning + rego: | + package waf + violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { + not "graphql-waf" in input.source.namespacePlugins + } + ``` + + Being advisory, it's safe to hold yourself to a stricter standard than the baseline — e.g. a higher minimum paranoia level. Violations surface on `WAFPolicy` status without interrupting traffic. + +## Read violation results + +Violations from matching policies appear in the affected `WAFPolicy`'s status conditions. + +1. Check the policy status: + + ```bash + kubectl get wafpolicy -n -o yaml + ``` + + A blocked policy shows the failing rule: + + ```yaml + status: + conditions: + - type: Ready + status: "False" + reason: ValidationFailed + message: > + GlobalWAFValidationPolicy/production-requirements rule "block-mode-required": + WAF must be in Block mode for production + - type: Accepted + status: "True" + ``` + +An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (the filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. :::tip Write Rego against the schema in [Rego validation](./reference.mdx#rego-validation); test locally with `opa eval` first. A syntax error invalidates the whole policy — status reports which rule failed to parse. ::: + +## Next steps + +- [Multi-tenant setup](./multi-tenant-setup.mdx) — baseline, plugins, and guardrails working together on a shared cluster. +- [Reference](./reference.mdx) — the Rego input schema, worked examples, and status conditions. +- [Troubleshooting](./troubleshooting.mdx) — diagnose a policy blocked by validation. diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index 3e21ac85f8..f02de7bf09 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -4,14 +4,13 @@ description: Write custom ModSecurity rules in a WAFPlugin or GlobalWAFPlugin, u # Write custom rules with plugins -A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives in four lifecycle slots, modeled on the Core Rule Set 4 plugin structure — for app-specific detections and false-positive exceptions the Core Rule Set doesn't cover on its own. Cluster operators wanting the same rules org-wide reference `GlobalWAFPlugin` from `GlobalWAFPolicy.spec.plugins[]` instead — same fields, different scope. Slot choice follows [load order](./index.mdx#the-merged-ruleset-load-order): +:::note + +This feature is tech preview. Tech preview features may be subject to significant changes before they become GA. + +::: -| Slot | Runs | Use for | -| :--- | :--- | :--- | -| `config` | Before Core Rule Set setup | `tx.*` variable setup (anomaly thresholds, optional features, app flags). Rules on request variables belong in `before` instead — the request isn't read yet | -| `before` | After variable setup, before Core Rule Set loads | Early-exit allowlisting of trusted paths, request normalization, flags for later rules | -| `rules` | Alongside Core Rule Set, after it (Tigera extension) | App-specific detections that complement rather than pre-empt the Core Rule Set; can act on anomaly scores | -| `after` | Last | False-positive suppression, response inspection, logging. `GlobalWAFPlugin.after` runs *after* namespace `WAFPlugin.after`, so the cluster operator has final say | +A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecurity SecLang](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)) directives in four lifecycle slots — `config`, `before`, `rules`, `after` — for app-specific detections and false-positive exceptions the Core Rule Set doesn't cover on its own. Cluster operators wanting the same rules org-wide use `GlobalWAFPlugin` instead — same fields, different scope. For what each slot does and when it runs, see [Plugins — custom rules](./overview.mdx#plugins--custom-rules). :::caution @@ -25,106 +24,116 @@ A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecuri ::: -## Rule IDs and your namespace's range - -Every `SecRule`/`SecAction` needs an ID. Each namespace gets a 10,000-ID block, enforced at admission. Find your range first: - -```bash -kubectl get namespace \ - -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' -# example output: 420000-429999 -``` - -Populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. - -## Worked example - -This `WAFPlugin` uses all four slots to protect a GraphQL API: - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPlugin -metadata: - name: graphql-waf - namespace: platform -spec: - description: "GraphQL introspection guard and false-positive exceptions" - - config: | - # Flag the application type so later rules can branch on it. - SecAction \ - "id:420001,phase:1,pass,nolog,\ - setvar:tx.is_graphql=1" - - before: | - # Allowlist the schema endpoint used by the internal schema registry — - # skip WAF analysis entirely for that specific path. - SecRule REQUEST_URI "@streq /graphql/schema-registry" \ - "id:420010,phase:1,pass,nolog,\ - ctl:ruleEngine=Off,\ - tag:allowlist" - - rules: | - # Block introspection queries (production guard). - SecRule TX:IS_GRAPHQL "@eq 1" \ - "id:420020,phase:2,deny,status:403,\ - msg:'GraphQL introspection blocked',\ - logdata:'body_snippet=%{MATCHED_VAR}',\ - chain" - SecRule REQUEST_BODY "@rx __schema|__type" "" - - # Block suspicious user-agents known to scan GraphQL APIs. - SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ - "id:420021,phase:1,deny,status:403,\ - msg:'Known GraphQL scanner blocked',\ - logdata:'ua=%{MATCHED_VAR}'" - - after: | - # Remove a false positive: CRS rule 941130 fires on a legitimate - # Content-Type value used by our GraphQL client library. - SecRuleRemoveById 941130 -``` - -:::tip - -`@pmf` loads a data file, which must be bundled into the Coraza WASM filter image — ask your cluster operator, or use `@pm` (inline phrase match) for a short string list. +## Prerequisites + +- Gateway WAF is enabled. See [Enable Gateway WAF](./enable.mdx). +- You have write access to `WAFPlugin` in your namespace (or `GlobalWAFPlugin` at cluster scope). +- A `WAFPolicy` or `GlobalWAFPolicy` exists to reference the plugin from — a plugin does nothing until a policy loads it. See [Attach WAF to a Gateway or route](./attach-waf-to-a-route.mdx). -::: +## Write a plugin -A plugin has no effect until a `WAFPolicy` references it, in `spec.plugins[]` (load order = array position): - -```yaml -apiVersion: applicationlayer.projectcalico.org/v3 -kind: WAFPolicy -metadata: - name: platform-api-waf - namespace: platform -spec: - targetRefs: - - group: gateway.networking.k8s.io - kind: HTTPRoute - name: graphql-api - action: Block - plugins: - - kind: WAFPlugin - name: graphql-waf # ← your plugin above -``` - -If the policy also references a `GlobalWAFPlugin`, its slots load before the namespace plugin's slots at each position. +1. Find your namespace's SecRule ID range. Every `SecRule`/`SecAction` needs an ID, and each namespace gets a 10,000-ID block enforced at admission: -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. + ```bash + kubectl get namespace \ + -o jsonpath='{.metadata.annotations.applicationlayer\.projectcalico\.org/waf-id-range}' + # example output: 420000-429999 + ``` -:::note + The range is populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. -Not all SecLang directives are permitted — directives that read the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. +1. Write the `WAFPlugin`, placing each rule in the slot that matches when it should fire. This example uses all four slots to protect a GraphQL API: -::: + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: WAFPlugin + metadata: + name: graphql-waf + namespace: platform + spec: + description: "GraphQL introspection guard and false-positive exceptions" + + config: | + # Flag the application type so later rules can branch on it. + SecAction \ + "id:420001,phase:1,pass,nolog,\ + setvar:tx.is_graphql=1" + + before: | + # Allowlist the schema endpoint used by the internal schema registry — + # skip WAF analysis entirely for that specific path. + SecRule REQUEST_URI "@streq /graphql/schema-registry" \ + "id:420010,phase:1,pass,nolog,\ + ctl:ruleEngine=Off,\ + tag:allowlist" + + rules: | + # Block introspection queries (production guard). + SecRule TX:IS_GRAPHQL "@eq 1" \ + "id:420020,phase:2,deny,status:403,\ + msg:'GraphQL introspection blocked',\ + logdata:'body_snippet=%{MATCHED_VAR}',\ + chain" + SecRule REQUEST_BODY "@rx __schema|__type" "" + + # Block suspicious user-agents known to scan GraphQL APIs. + SecRule REQUEST_HEADERS:User-Agent "@pmf graphql-scanners.dat" \ + "id:420021,phase:1,deny,status:403,\ + msg:'Known GraphQL scanner blocked',\ + logdata:'ua=%{MATCHED_VAR}'" + + after: | + # Remove a false positive: CRS rule 941130 fires on a legitimate + # Content-Type value used by our GraphQL client library. + SecRuleRemoveById 941130 + ``` -Confirm acceptance and reconciliation: + :::tip + + `@pmf` loads a data file, which must be bundled into the Coraza WASM filter image — ask your cluster operator, or use `@pm` (inline phrase match) for a short string list. + + ::: + +1. Reference the plugin from a `WAFPolicy`, in `spec.plugins[]` (load order = array position): + + ```yaml + apiVersion: applicationlayer.projectcalico.org/v3 + kind: WAFPolicy + metadata: + name: platform-api-waf + namespace: platform + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: graphql-api + action: Block + plugins: + - kind: WAFPlugin + name: graphql-waf # ← your plugin above + ``` + + If the policy also references a `GlobalWAFPlugin`, its slots load before the namespace plugin's slots at each position. + + :::note + + Not all SecLang directives are permitted — directives that read the filesystem, fetch remote rules, or write to arbitrary audit log paths are rejected. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for the allowlist. + + ::: + +1. Confirm acceptance and reconciliation: + + ```bash + kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" + kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" + ``` + + `Ready=True` on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed. + +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. -```bash -kubectl get wafplugin graphql-waf -n platform -o yaml | grep -A10 "conditions:" -kubectl get wafpolicy platform-api-waf -n platform -o yaml | grep -A20 "conditions:" -``` +## Next steps -`Ready=True` on the `WAFPolicy` confirms the merged ruleset passed validation and the `EnvoyExtensionPolicy` was programmed. +- [Validate configuration with Rego](./validate-config.mdx) — enforce guardrails on the merged ruleset. +- [Reference](./reference.mdx) — SecRule ID ranges, allowed directives, and status conditions. +- [Troubleshooting](./troubleshooting.mdx) — diagnose a plugin that won't accept or program. diff --git a/sidebars-calico-enterprise.js b/sidebars-calico-enterprise.js index b949f8e93c..83ea59dd77 100644 --- a/sidebars-calico-enterprise.js +++ b/sidebars-calico-enterprise.js @@ -478,9 +478,8 @@ module.exports = { { type: 'category', label: 'Gateway WAF', - link: { type: 'doc', id: 'threat/gateway-waf/index' }, items: [ - 'threat/gateway-waf/get-started', + 'threat/gateway-waf/overview', 'threat/gateway-waf/tutorial', 'threat/gateway-waf/enable', 'threat/gateway-waf/set-a-baseline', From 1720e165c086db379e96f476dc901bf7d1941c7b Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Tue, 14 Jul 2026 09:50:58 +0100 Subject: [PATCH 15/16] chore(ci): retrigger netlify deploy preview Previous preview failed on a transient link-checker flake: external YouTube links 429'd on unrelated pages. No content change. From 849e5f151ab2159a8c38cac1cf2cd5cda5b5d355 Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Tue, 14 Jul 2026 16:40:27 +0100 Subject: [PATCH 16/16] fix(gateway-waf): correct tutorial namespace and severity enum casing EV-6809. Two copy-paste-breaking errors found in the docs review: - The Envoy service and pods land in the Gateway's own namespace, not a `tigera-gateway` namespace (which doesn't exist). Point the tutorial service lookup, in-cluster traffic URLs, and the troubleshooting log commands at the real location. - The validation-rule severity enum ships lowercase (info/warning/critical), so the CRD rejected the capitalized values in the examples. Lowercase the YAML values and the matching prose across the section. securityPosture is a separate, genuinely capitalized enum and is left alone. Also: the tutorial's Programmed reason is ConfigurationApplied, not Programmed; and note that the plugin example rule IDs must come from your own namespace's allocated range or admission rejects them. --- .../threat/gateway-waf/multi-tenant-setup.mdx | 2 +- .../threat/gateway-waf/overview.mdx | 8 ++++---- .../threat/gateway-waf/reference.mdx | 14 +++++++------- .../threat/gateway-waf/troubleshooting.mdx | 6 +++--- .../threat/gateway-waf/tutorial.mdx | 14 +++++++------- .../threat/gateway-waf/validate-config.mdx | 16 ++++++++-------- .../threat/gateway-waf/write-custom-rules.mdx | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx index 6213022289..a7a5d754bf 100644 --- a/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx +++ b/calico-enterprise/threat/gateway-waf/multi-tenant-setup.mdx @@ -67,7 +67,7 @@ A cluster shared by `team-a` and `payments`, with a cluster operator setting the ## What happens if a team tries to weaken the baseline -If `payments` applies `action: Detect`, `production-requirements` (`Enforce`) fires its `must-use-block-mode` Critical rule — the controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation: +If `payments` applies `action: Detect`, `production-requirements` (`Enforce`) fires its `must-use-block-mode` critical rule — the controller marks the policy `Programmed=False, Reason=NotAttempted` and surfaces the violation: ```bash kubectl get wafpolicy payments-waf -n payments \ diff --git a/calico-enterprise/threat/gateway-waf/overview.mdx b/calico-enterprise/threat/gateway-waf/overview.mdx index 335a76bf4d..f591995beb 100644 --- a/calico-enterprise/threat/gateway-waf/overview.mdx +++ b/calico-enterprise/threat/gateway-waf/overview.mdx @@ -26,7 +26,7 @@ Gateway WAF is configured entirely through Kubernetes resources. The controller
Gateway WAF request flow. GlobalWAFPolicy/WAFPolicy and GlobalWAFPlugin/WAFPlugin are merged into an ordered list of SecLang directives. Rego validation then either passes the result to an EnvoyExtensionPolicy that loads the Coraza WASM filter on Envoy Gateway, or, on an Enforce-mode Critical violation, programs no filter and reports the violation on status. The admission webhook validates submitted rules at apply time. The inputs to the merge are `GlobalWAFPolicy` + `WAFPolicy` and `GlobalWAFPlugin` + `WAFPlugin`. Two guardrails apply: the admission webhook (structural, at write time) and validation policies (compliance, after the merge). @@ -74,7 +74,7 @@ For the full merge sequence across every policy and plugin, see [merged ruleset A **validation policy** carries [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) rules evaluated against the *merged, effective* configuration — answering "is this acceptable?", e.g. "Core Rule Set must stay enabled" or "production must run in Block mode." -- `GlobalWAFValidationPolicy` — enforced by the cluster operator: `Enforce` mode blocks programming on a `Critical` violation, `Audit` mode only warns. `namespaceSelector` scopes which namespaces it applies to. +- `GlobalWAFValidationPolicy` — enforced by the cluster operator: `Enforce` mode blocks programming on a `critical` violation, `Audit` mode only warns. `namespaceSelector` scopes which namespaces it applies to. - `WAFValidationPolicy` — advisory only, surfaces violations on status for the application operator to act on, never blocks. No enforce mode at namespace scope. (Validation policies aren't the admission webhook — see [Validation and enforcement](#validation-and-enforcement) for the distinction.) @@ -180,9 +180,9 @@ This stops one namespace colliding with another's rule IDs, or disabling the clu The admission webhook checks *raw SecLang*; validation policies check the *merged result*, running after the merge because some problems only show up there. Two innocent plugins can combine to disable all SQL-injection rules — invisible to admission-time checks, but visible to a Rego rule against the merged directive list. -Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with severity `Info`, `Warning`, or `Critical`. What happens on a violation depends on the policy: +Validation rules are [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) modules with severity `info`, `warning`, or `critical`. What happens on a violation depends on the policy: -| Validation policy | Mode | `Critical` violation | `Warning` / `Info` violation | +| Validation policy | Mode | `critical` violation | `warning` / `info` violation | | :--- | :--- | :--- | :--- | | `GlobalWAFValidationPolicy` | `Enforce` | **blocks** — no filter is programmed; status reports the rejection | warns on status; filter still programmed | | `GlobalWAFValidationPolicy` | `Audit` (default) | warns on status; filter still programmed | warns on status; filter still programmed | diff --git a/calico-enterprise/threat/gateway-waf/reference.mdx b/calico-enterprise/threat/gateway-waf/reference.mdx index 706ecb1882..aed22524e9 100644 --- a/calico-enterprise/threat/gateway-waf/reference.mdx +++ b/calico-enterprise/threat/gateway-waf/reference.mdx @@ -130,7 +130,7 @@ spec: env: production rules: - name: crs-must-be-enabled - severity: Critical + severity: critical rego: | package waf violations[{"msg": "OWASP CRS must be enabled"}] { @@ -144,12 +144,12 @@ spec: | Field | Type | Resource | Notes | | :--- | :--- | :--- | :--- | -| `enforcementMode` | `Audit` \| `Enforce` | **GlobalWAFValidationPolicy only** | Defaults to `Audit`. `Enforce` blocks on a `Critical` violation | +| `enforcementMode` | `Audit` \| `Enforce` | **GlobalWAFValidationPolicy only** | Defaults to `Audit`. `Enforce` blocks on a `critical` violation | | `namespaceSelector` | label selector | **GlobalWAFValidationPolicy only** | Empty = all namespaces | | `rules[]` | list of `ValidationRule` | both | At least one required | | `rules[].name` | string | both | 1–253 chars | | `rules[].rego` | string (Rego module) | both | Must declare `package waf` and a `violations` rule; max 16384 chars | -| `rules[].severity` | `Info` \| `Warning` \| `Critical` | both | Defaults to `Warning` | +| `rules[].severity` | `info` \| `warning` \| `critical` | both | Defaults to `warning` | --- @@ -217,9 +217,9 @@ How different merged configurations evaluate against the rules above: ### Severity -Each rule has a `severity` — `Info`, `Warning`, `Critical` (default `Warning`) — deciding the consequence, not whether it fires: +Each rule has a `severity` — `info`, `warning`, `critical` (default `warning`) — deciding the consequence, not whether it fires: -- In `Enforce` mode, a `Critical` violation blocks the configuration from being programmed. +- In `Enforce` mode, a `critical` violation blocks the configuration from being programmed. - In `Audit` mode — and in any `WAFValidationPolicy` — violations are reported on status but never block. See the enforcement table in [How it works](./overview.mdx#validation-and-enforcement) for the full matrix. @@ -324,7 +324,7 @@ Whether the resource is structurally valid and admitted. Several reasons come st | :--- | :--- | | `Accepted` | The resource is valid and admitted | | `Invalid` | The spec or a directive is invalid (the message says which) | -| `ValidationFailed` | A `GlobalWAFValidationPolicy` in `Enforce` mode reported a `Critical` violation | +| `ValidationFailed` | A `GlobalWAFValidationPolicy` in `Enforce` mode reported a `critical` violation | | `Conflicted` | Another policy already targets this route; the older one wins (the message names it) | | `PluginNotFound` | A referenced `WAFPlugin`/`GlobalWAFPlugin` does not exist | | `UnsafeDirective` | A directive is not on the allowlist | @@ -351,7 +351,7 @@ Whether the Coraza WASM filter was programmed into Envoy Gateway. | `TargetNotFound` | No valid `targetRefs`, or a referenced `Gateway`/`HTTPRoute` does not exist | | `RenderTooLarge` | The merged ruleset exceeds the render size limit | | `WASMUnavailable` | The Coraza WASM image could not be pulled | -| `NotAttempted` | A `Critical` violation under `Enforce` blocked programming | +| `NotAttempted` | A `critical` violation under `Enforce` blocked programming | ### Ready diff --git a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx index 63596f7d04..93661f150c 100644 --- a/calico-enterprise/threat/gateway-waf/troubleshooting.mdx +++ b/calico-enterprise/threat/gateway-waf/troubleshooting.mdx @@ -84,7 +84,7 @@ kubectl get envoyextensionpolicy \ Check Envoy proxy logs for `could not fetch Wasm OCI image`: ```bash -kubectl logs -n tigera-gateway \ +kubectl logs -n \ -l gateway.envoyproxy.io/owning-gateway-namespace= \ -c envoy | grep -i wasm ``` @@ -95,7 +95,7 @@ kubectl logs -n tigera-gateway \ ## `Programmed=False, Reason=NotAttempted` -**Cause.** A `GlobalWAFValidationPolicy` in `Enforce` mode found a `Critical` violation, so the controller never programmed the Coraza filter. +**Cause.** A `GlobalWAFValidationPolicy` in `Enforce` mode found a `critical` violation, so the controller never programmed the Coraza filter. **Diagnostic.** Read the validation failures: @@ -163,7 +163,7 @@ See [How it works](./overview.mdx#inheritance-namespaces-override-they-dont-rest **Diagnostic.** The firing rule ID and message are in the `tigera_secure_ee_waf` log index for the hit. As a last resort, the Envoy proxy access logs in the Gateway pod also carry them: ```bash -kubectl logs -n tigera-gateway \ +kubectl logs -n \ -l gateway.envoyproxy.io/owning-gateway-namespace= \ -c envoy | grep -i "waf\|403" | tail -30 ``` diff --git a/calico-enterprise/threat/gateway-waf/tutorial.mdx b/calico-enterprise/threat/gateway-waf/tutorial.mdx index bc242fe5cc..2029d1c8e7 100644 --- a/calico-enterprise/threat/gateway-waf/tutorial.mdx +++ b/calico-enterprise/threat/gateway-waf/tutorial.mdx @@ -114,10 +114,10 @@ This tutorial takes you from a running Ingress Gateway to a blocked attack in ab kubectl wait --for=condition=Accepted gateway/httpbin-gateway -n httpbin --timeout=60s ``` -1. Confirm traffic flows. Find the Envoy service the Ingress Gateway created for this Gateway (services are created in the `tigera-gateway` namespace): +1. Confirm traffic flows. Find the Envoy service the Ingress Gateway created for this Gateway (the service lands in the Gateway's own namespace, `httpbin`): ```bash - export GATEWAY_SVC=$(kubectl get svc -n tigera-gateway \ + export GATEWAY_SVC=$(kubectl get svc -n httpbin \ -o jsonpath='{.items[?(@.metadata.labels.gateway\.envoyproxy\.io/owning-gateway-name=="httpbin-gateway")].metadata.name}') ``` @@ -126,7 +126,7 @@ This tutorial takes you from a running Ingress Gateway to a blocked attack in ab ```bash kubectl run curl-test -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get + http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get ``` ```text @@ -183,7 +183,7 @@ This tutorial takes you from a running Ingress Gateway to a blocked attack in ab Licensed=True (LicenseValid) Accepted=True (Accepted) Validated=Unknown (NotEvaluated) - Programmed=True (Programmed) + Programmed=True (ConfigurationApplied) Ready=True (Ready) ``` @@ -195,17 +195,17 @@ This tutorial takes you from a running Ingress Gateway to a blocked attack in ab # Normal request — expect 200 kubectl run curl-ok -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ curl -s -o /dev/null -w "%{http_code}" \ - http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get + http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get # SQL injection probe — expect 403 kubectl run curl-sqli -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" + "http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get?id=1%27%20OR%20%271%27%3D%271" # XSS probe — expect 403 kubectl run curl-xss -n httpbin --rm -it --restart=Never --image=curlimages/curl -- \ curl -s -o /dev/null -w "%{http_code}" \ - "http://$GATEWAY_SVC.tigera-gateway.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" + "http://$GATEWAY_SVC.httpbin.svc.cluster.local:9080/get?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" ``` The Coraza WASM filter, running inside Envoy, evaluated each request against the OWASP Core Rule Set. Core Rule Set rule group 942xxx detects SQL injection; group 941xxx detects cross-site scripting. Both attacks are blocked before the request reaches the application. diff --git a/calico-enterprise/threat/gateway-waf/validate-config.mdx b/calico-enterprise/threat/gateway-waf/validate-config.mdx index 54f9ebed4e..cfe79d08bd 100644 --- a/calico-enterprise/threat/gateway-waf/validate-config.mdx +++ b/calico-enterprise/threat/gateway-waf/validate-config.mdx @@ -20,7 +20,7 @@ Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang a ## Enforce guardrails cluster-wide with GlobalWAFValidationPolicy -`GlobalWAFValidationPolicy` is cluster-scoped, applying to every namespace or a label-selected subset via `namespaceSelector`. It carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `Critical` violation. `Warning`/`Info` violations are always recorded. +`GlobalWAFValidationPolicy` is cluster-scoped, applying to every namespace or a label-selected subset via `namespaceSelector`. It carries Rego rules and an `enforcementMode`: `Audit` (default) records violations while the filter stays programmed; `Enforce` blocks programming on a `critical` violation. `warning`/`info` violations are always recorded. 1. Write the policy with Rego rules and an `enforcementMode`. This one requires the Core Rule Set enabled and `Block` mode for namespaces labeled `env: production`: @@ -36,18 +36,18 @@ Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang a env: production rules: - # Critical: CRS must be enabled + # critical: CRS must be enabled - name: crs-must-be-enabled - severity: Critical + severity: critical rego: | package waf violations[{"msg": "OWASP CRS must be enabled"}] { input.config.crsState != "Enabled" } - # Critical: production namespaces must use Block mode + # critical: production namespaces must use Block mode - name: block-mode-required - severity: Critical + severity: critical rego: | package waf violations[{"msg": "WAF must be in Block mode for production"}] { @@ -59,7 +59,7 @@ Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang a 1. Start in `Audit` mode, review violations on existing `WAFPolicy` status, then switch to `Enforce` — so you don't block teams whose config predates the policy. - Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `Critical` violation (in `Enforce`) blocks programming. + Leave `namespaceSelector` empty for cluster-wide, or scope with `matchLabels`. Multiple policies can coexist — every matching rule is evaluated, and one `critical` violation (in `Enforce`) blocks programming. ## Self-check your config with WAFValidationPolicy @@ -77,7 +77,7 @@ Gateway WAF has two guardrail layers: the admission webhook checks raw SecLang a rules: - name: graphql-plugin-active - severity: Warning + severity: warning rego: | package waf violations[{"msg": "graphql-waf plugin is not loaded — introspection unguarded"}] { @@ -112,7 +112,7 @@ Violations from matching policies appear in the affected `WAFPolicy`'s status co status: "True" ``` -An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [Warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (the filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. +An audit violation instead leaves `Ready=True` and adds a `ValidationWarning=True` condition, reason `AuditViolation`, with a message like `rce-rules-intact" [warning]: RCE rule removal detected in merged config: SecRuleRemoveById 932100` (the filter stays programmed). See [Status conditions](./reference.mdx#status-conditions) for the full reference. :::tip diff --git a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx index f02de7bf09..65a86b7a57 100644 --- a/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx +++ b/calico-enterprise/threat/gateway-waf/write-custom-rules.mdx @@ -42,7 +42,7 @@ A `WAFPlugin` (namespace) or `GlobalWAFPlugin` (cluster) holds custom [ModSecuri The range is populated the first time a plugin is referenced in your namespace. See [SecRule ID ranges](./reference.mdx#secrule-id-ranges) for protected ranges and the allowlist. -1. Write the `WAFPlugin`, placing each rule in the slot that matches when it should fire. This example uses all four slots to protect a GraphQL API: +1. Write the `WAFPlugin`, placing each rule in the slot that matches when it should fire. This example uses all four slots to protect a GraphQL API. The `4200xx` IDs below assume the `420000-429999` example range from the previous step — swap in IDs from your own namespace's range, or the admission webhook rejects the plugin with `IDOutsideRange`: ```yaml apiVersion: applicationlayer.projectcalico.org/v3