Skip to main content
Calico Cloud documentation

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.

ResourceScopeShort namePersonaPurpose
GlobalWAFPolicyClustergwafpCOCluster-wide baseline (singleton, name: default)
WAFPolicyNamespacewafpAOAttach WAF to a target; per-namespace overrides
GlobalWAFPluginClustergwafpluginCOOrganization-wide custom rules
WAFPluginNamespacewafpluginAOApp-specific rules and exceptions
GlobalWAFValidationPolicyClustergwafvpCOCompliance guardrails (enforce or audit)
WAFValidationPolicyNamespacewafvpAOAdvisory self-validation

Policy family​

GlobalWAFPolicy sets the cluster baseline; WAFPolicy attaches WAF to a target and overrides the baseline for its namespace.

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 adds targetRefs (Gateway/HTTPRoute); action and coreRuleSet are optional overrides of the global defaults:

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

FieldTypeResourceNotes
defaultActionDetect | BlockGlobalWAFPolicyCluster default; defaults to Detect
actionDetect | BlockWAFPolicyOptional; nil = inherit the global default
coreRuleSet.stateEnabled | DisabledbothDefaults to Enabled; nil on WAFPolicy = inherit
coreRuleSet.paranoiaLevelinteger 1–4bothDefaults to 1; nil on WAFPolicy = inherit
plugins[]list of {kind, name}bothkind: WAFPlugin | GlobalWAFPlugin; order = array position; max 64
targetRefs[]Gateway API target refsWAFPolicy onlykind: Gateway | HTTPRoute; min 1, max 16. sectionName optionally scopes a ref to a specific listener/section

Plugin family​

GlobalWAFPlugin/WAFPlugin share an identical spec: custom ModSecurity rules in four lifecycle slots — see Write custom rules.

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

FieldTypeNotes
descriptionstringHuman-readable purpose; max 1024 chars
configstring (SecLang)Slot 1 — tx.* variable setup, runs before Core Rule Set setup; max 65536 chars
beforestring (SecLang)Slot 2 — pre-Core Rule Set rules; max 65536 chars
rulesstring (SecLang)Slot 3 — custom rules alongside Core Rule Set (Tigera extension); max 65536 chars
afterstring (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.

Validation family​

GlobalWAFValidationPolicy can enforce; WAFValidationPolicy is advisory only. Both carry Rego rules — see Rego validation.

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, advisory only — no enforcementMode, no namespaceSelector; every rule runs as if in Audit mode.

Field reference — Validation family

FieldTypeResourceNotes
enforcementModeAudit | EnforceGlobalWAFValidationPolicy onlyDefaults to Audit. Enforce blocks on a critical violation
namespaceSelectorlabel selectorGlobalWAFValidationPolicy onlyEmpty = all namespaces
rules[]list of ValidationRulebothAt least one required
rules[].namestringboth1–253 chars
rules[].regostring (Rego module)bothMust declare package waf and a violations rule; max 16384 chars
rules[].severityinfo | warning | criticalbothDefaults to warning

Rego validation​

A validation policy carries Rego 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 evaluates against the same input document — the merged configuration and its provenance:

PathTypeMeaning
input.config.actionstringEffective action after merge — Detect or Block
input.config.crsStatestringCore Rule Set state — Enabled or Disabled
input.config.paranoiaLevelintegerEffective paranoia level, 1–4
input.directivesarray of stringThe complete, ordered list of merged ModSecurity directives
input.source.globalPolicystringName of the global policy applied (always default)
input.source.namespacePolicystringName of the WAFPolicy, if any
input.source.globalPluginsarray of stringGlobal plugin names, in load order
input.source.namespacePluginsarray of stringNamespace plugin names, in load order

The violations contract​

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.

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])
}

Or inspect the merged directive list directly — useful for catching a plugin that removes a baseline rule:

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​

How different merged configurations evaluate against the rules above:

Effective configurationviolations
crsState: Enabled, paranoiaLevel: 2empty — 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 — 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 Audit mode — and in any WAFValidationPolicy — violations are reported on status but never block.

See the enforcement table in How it works for the full matrix.


SecRule ID ranges​

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​

Each namespace is allocated a contiguous block of 10,000 IDs, derived deterministically from the namespace name:

prefix = first four hex digits of sha256(namespace name), modulo 10000
range = [ prefix * 10000 , prefix * 10000 + 9999 ]

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​

The first time a namespace references a plugin, the controller stamps the range onto it as an annotation:

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 — stopping a tenant from quietly disabling cluster protection.

What the admission webhook rejects​

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.

CheckRejected withMeaning
Directive not on the allowlistUnsafeDirectiveThe directive type isn't permitted (for example, one that reads files or fetches remote rules)
Rule ID outside your rangeIDOutsideRangeA SecRule/SecAction ID falls outside the namespace's allocated range
Removing a protected ruleProtectedIDRemovalA SecRuleRemoveById targets an ID outside the namespace's range (such as a Core Rule Set baseline rule)

The controller re-runs these checks at reconcile as a backstop, marking Accepted=False with the matching reason instead of programming.

Allowed directives​

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​

A namespace whose range is 420000–429999 writes rules using IDs from that block:

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 policy state through standard Kubernetes status conditions:

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, Ready=True, evaluated roughly in that order — an early failure explains why later conditions are absent or False.

Licensed​

This condition reports whether the cluster is entitled to Gateway WAF.

ReasonMeaning
LicenseValidThe cluster is entitled to this feature
LicenseRequiredNo valid entitlement; the policy is not programmed

Accepted​

Whether the resource is structurally valid and admitted. Several reasons come straight from the admission webhook.

ReasonMeaning
AcceptedThe resource is valid and admitted
InvalidThe spec or a directive is invalid (the message says which)
ValidationFailedA GlobalWAFValidationPolicy in Enforce mode reported a critical violation
ConflictedAnother policy already targets this route; the older one wins (the message names it)
PluginNotFoundA referenced WAFPlugin/GlobalWAFPlugin does not exist
UnsafeDirectiveA directive is not on the allowlist
IDOutsideRangeA rule ID is outside the namespace's allocated range
ProtectedIDRemovalA SecRuleRemoveById targets a protected baseline rule

Validated​

The result of evaluating validation policies against the merged configuration.

ReasonMeaning
CompliantNo violations
ViolationsFoundOne or more rules reported a violation (see status.validation.failures)
NotEvaluatedNo validation policy applies to this resource

Programmed​

Whether the Coraza WASM filter was programmed into Envoy Gateway.

ReasonMeaning
ConfigurationAppliedThe EnvoyExtensionPolicy was generated and the filter is active
TargetNotFoundNo valid targetRefs, or a referenced Gateway/HTTPRoute does not exist
RenderTooLargeThe merged ruleset exceeds the render size limit
WASMUnavailableThe Coraza WASM image could not be pulled
NotAttemptedA critical violation under Enforce blocked programming

Ready​

A roll-up of the conditions above.

ReasonMeaning
ReadyThe policy is active on the data plane
NotReadyOne of the conditions above is not satisfied

Validation summary​

WAFPolicy.status.validation summarizes validation without reading individual conditions:

FieldMeaning
statusValid, Audited, or Rejected
securityPostureCompliant, Warning, Degraded, or Critical
failures[]Per-rule failures — policy name, rule, severity, and message
lastEvaluatedWhen validation last ran

A GlobalWAFValidationPolicy's own status aggregates violating namespaces cluster-wide — one place to see who's out of compliance.