Skip to content

CEL Policy Matching Reference ​

CEL policy matching is the shared request matcher used by WAF rules, rate-limit rules, traffic shaper rules, and cache rules.

What It Is ​

match_rule is a request-only policy matcher. It can be configured with the management UI Builder mode or with raw CEL in CEL mode.

An empty or omitted match_rule matches any request.

Builder mode generates a CEL expression from grouped conditions. For non-empty builder rules, p2pstream stores both the structured builder and the generated cel_expression so the API can validate that both describe the same rule.

Where It Runs ​

Policy match timing depends on the feature using the matcher:

FeatureMatch timing
WAFBefore rate limits, traffic shapers, route resolution, and forwarding.
Rate limitsAfter WAF, before traffic shaping and route resolution.
Traffic shapersAfter WAF and rate limits, before route/target forwarding.
CacheAfter route/target selection, before upstream forwarding.
RetriesAfter route/target selection and a cache miss, before agent upstream attempts.

Cache and retry route_ids and target_ids are separate post-routing filters. They are not CEL variables.

API Shape ​

PublicPolicyMatchRule has two fields:

FieldUse
cel_expressionThe CEL expression evaluated for each request.
builderStructured groups and conditions that can generate a CEL expression.

When both fields are supplied, cel_expression must exactly match the expression generated from builder.

Policy APIs accept match_rule with CEL and optional builder JSON. Stored policy rules must use this supported format.

Variables ​

VariableTypeNotes
methodstringUppercase request method, such as GET or POST.
protocolstringListener protocol, http or https.
hoststringNormalized request host without port.
pathstringDecoded URL path as seen by p2pstream policy layers.
remote_ipstringResolved visitor IP. It is the connection peer by default, or a strictly parsed visitor header only when that peer belongs to an explicitly trusted proxy source. It is empty when trusted-proxy resolution is contradictory or malformed.
headersmap string to list stringHeader names are lowercase; repeated values are preserved.
cookiesmap string to stringFirst cookie value by name.
querymap string to list stringQuery parameter values by name.

Helper Functions ​

host_match(host, pattern) checks an exact or wildcard host pattern.

text
host_match(host, "app.example.com")
host_match(host, "*.example.com")

path_prefix(path, prefix) checks a p2pstream path prefix. The prefix must start with /.

text
path_prefix(path, "/api")
path_prefix(path, "/assets")

cidr(remote_ip, cidr) checks whether remote_ip is inside a valid IP prefix. Invalid IPs evaluate false. Literal invalid CIDR values are rejected during validation when p2pstream can see them statically.

text
cidr(remote_ip, "198.51.100.0/24")
cidr(remote_ip, "2001:db8::/32")

Common CEL Operators ​

ExpressionMeaning
method == "POST"Exact string equality.
method in ["GET", "HEAD"]Value is one of a list.
path.startsWith("/api/")String prefix check.
path.endsWith(".css")String suffix check.
host.contains("example")String contains check.
path.matches("^/assets/.+\\.js$")Regex match.
"x-plan" in headersHeader is present. Header names are lowercase.
headers["x-plan"].exists(v, v == "free")Any repeated header value matches.
query["version"].exists(v, v in ["v1", "v2"])Any query value matches.
!("session" in cookies)Cookie is absent.
A && BBoth expressions must be true.
`A
!(A)Expression must be false.

Builder Mode Mapping ​

Builder mode generates CEL from groups and conditions:

Builder itemCEL behavior
All groupJoins child expressions with &&.
Any groupJoins child expressions with `
Not group or conditionWraps the generated expression with !().
Header conditionChecks all repeated header values.
Query conditionChecks all repeated query values.
Cookie conditionChecks the first cookie value by name.
Present operatorValid only for header, cookie, and query fields.
CIDR operatorValid only for remote IP fields.
Host pattern operatorValid only for host fields.
Path prefix operatorValues must start with /.

Header and query conditions check all repeated values. Use an explicit CEL expression when a policy must inspect only the first value.

Limits And Validation ​

LimitValue
CEL expression max size4096 bytes
Builder max groups and conditions64 total nodes
Condition max values64
Condition value max size512 bytes
CEL evaluation cost limit20000

Expressions must compile and evaluate to bool. Regex literals are validated when p2pstream can see them statically. Regex arguments to .matches() must be string literals; dynamic regex patterns from request fields such as headers, cookies, or query parameters are rejected during policy validation and stored rule loading.

Evaluation is bounded by the cost limit. If a request exhausts that budget or otherwise causes evaluation to fail, WAF, rate-limit, and traffic-shaper enforcement fails closed with a local 503 Service Unavailable response. Cache evaluation errors bypass caching for that request, and retry evaluation errors skip retries. An evaluation error never selects a later cache or retry rule.

For routes that allow encoded path separators, CEL still receives the decoded path. Use route-scoped compatibility sparingly and avoid CEL authorization logic that depends on slash boundaries that an upstream interprets differently.

Literal arguments receive targeted validation:

  • cidr(remote_ip, "...") requires a valid CIDR prefix.
  • path_prefix(path, "...") requires a prefix starting with /.
  • host_match(host, "...") requires a non-empty host pattern.
  • value.matches("...") requires a string-literal regex no larger than the condition value limit.

Examples ​

Login POST protection:

text
method == "POST" && host_match(host, "app.example.com") && path_prefix(path, "/login")

Static asset cache matching:

text
method in ["GET", "HEAD"] && host_match(host, "app.example.com") && path.matches("^/assets/.+\\.(css|js|png|webp|svg|woff2)$")

Header or query tier selection:

text
headers["x-plan"].exists(v, v == "free") || query["tier"].exists(v, v == "free")

Bot user-agent match:

text
headers["user-agent"].exists(v, v.matches("(?i)(bot|crawler)"))

Invalid dynamic regex source:

text
path.matches(headers["x-re"][0])

Cookie absence:

text
!("session" in cookies)

IPv4 CIDR:

text
cidr(remote_ip, "198.51.100.0/24")

IPv6 CIDR:

text
cidr(remote_ip, "2001:db8::/32")

Combined host, path, and method rule:

text
method in ["GET", "POST"] && host_match(host, "*.example.com") && path_prefix(path, "/api")

What CEL Cannot See ​

CEL policy matches cannot inspect:

  • route data,
  • target data,
  • target health,
  • load-balancer state,
  • response data,
  • cache result,
  • request body.

For cache rules, route and target scoping must use the rule's route_ids and target_ids filters instead of CEL.

p2pstream may perform an internal route-only path security match before WAF, rate-limit, and traffic-shaper CEL evaluation. That match only selects the route path security mode; it does not expose route or target data to CEL and does not select a target.

Troubleshooting ​

SymptomFix
Rule matches every requestCheck whether match_rule is empty or the builder has no conditions. Empty matches mean any request.
Header rule does not matchUse lowercase header names such as headers["x-plan"]; p2pstream lowercases header keys.
Header or query value rule does not matchUse .exists(v, ...) because headers and query parameters are lists.
Regex is rejectedUse a string-literal regex, keep it within the value-size limit, compile it separately, and escape backslashes correctly inside the CEL string.
Path prefix is rejectedPrefix values for path_prefix and builder path-prefix conditions must start with /.
CIDR is rejected or never matchesUse a valid CIDR prefix and confirm p2pstream sees the expected client IP.
remote_ip is empty behind a CDNConfirm the CDN preset/custom peer CIDRs are enabled and current, and that the configured visitor-IP header is present and well formed.
Route or target data is missingCEL only sees request data. Use feature-specific route/target filters where available.

Operations documentation for self-hosted p2pstream deployments.