Matching

A route matches requests based on path, headers, methods, query parameters, hostnames, gRPC content-type, or CEL expressions. All specified matchers must pass (AND logic) for the route to match.

All match fields

FieldTypeDescription
pathstringExact path match
pathPrefixstringPath prefix match
pathRegexstringRE2 regex match
methodsstring[]HTTP methods (empty = all)
hostnamesstring[]Virtual host names (any must match)
headersobject[]Header matchers (all must match)
queryParamsobject[]Query parameter matchers (all must match)
grpcboolRestrict to gRPC content-type
celstringCEL expression for complex logic

Only one of path, pathPrefix, or pathRegex should be set.

Path matching

Exact path

{"match": {"path": "/health"}}

Matches only GET /health. Does not match /health/ or /health/deep.

Path prefix

{"match": {"pathPrefix": "/api/v1"}}

Matches /api/v1, /api/v1/users, /api/v1/orders/123, etc.

Path regex

{"match": {"pathRegex": "/users/[0-9]+"}}

Matches /users/42, /users/999. Does not match /users/abc. Regexes use RE2 syntax and are compiled once at routing table build time — zero per-request cost.

Complex regex

{"match": {"pathRegex": "^/api/(v[12])/users/[a-f0-9-]{36}$"}}

Matches /api/v1/users/550e8400-e29b-41d4-a716-446655440000 and /api/v2/users/....

Method matching

{"match": {"methods": ["GET", "POST"]}}

Only matches GET and POST requests. Empty array or omitted = matches all methods.

{"match": {"methods": ["DELETE"], "pathPrefix": "/api/admin"}}

Only matches DELETE requests to /api/admin/*.

Header matching

Exact header value

{"match": {"headers": [{"name": "X-Tenant", "value": "acme"}]}}

Matches requests with X-Tenant: acme.

Regex header value

{"match": {"headers": [{"name": "X-Version", "value": "v[0-9]+", "regex": true}]}}

Matches X-Version: v1, X-Version: v2, etc.

Multiple headers (AND)

{
  "match": {
    "headers": [
      {"name": "X-Tenant", "value": "acme"},
      {"name": "X-Env", "value": "production"}
    ]
  }
}

Both headers must be present and match.

Header presence (any value)

{"match": {"headers": [{"name": "Authorization"}]}}

Matches any request with an Authorization header, regardless of value.

Query parameter matching

Exact value

{"match": {"queryParams": [{"name": "format", "value": "json"}]}}

Matches ?format=json.

Regex value

{"match": {"queryParams": [{"name": "version", "value": "[0-9]+", "regex": true}]}}

Matches ?version=1, ?version=42, etc.

Multiple params (AND)

{
  "match": {
    "queryParams": [
      {"name": "format", "value": "json"},
      {"name": "page", "value": "[0-9]+", "regex": true}
    ]
  }
}

Both query params must be present and match.

Hostname matching

{"match": {"hostnames": ["api.example.com", "api.staging.example.com"]}}

The request’s Host header must match any one of the listed hostnames (OR logic). No wildcard support — use separate routes or CEL for complex hostname matching.

gRPC

{"match": {"grpc": true}}

Restricts the match to requests with Content-Type: application/grpc. Combine with other matchers:

{
  "match": {
    "grpc": true,
    "pathPrefix": "/mypackage.MyService"
  }
}

CEL expressions

For logic that static matchers can’t express:

{"match": {"cel": "request.path.startsWith('/api') && 'admin' in request.headers['x-role'] && request.method != 'DELETE'"}}

CEL is evaluated after all static matchers pass (it’s the most expensive check).

Available variables

VariableTypeDescription
request.methodstringHTTP method ("GET", "POST", etc.)
request.pathstringURL path
request.hoststringHostname without port
request.schemestring"http" or "https"
request.headersmapRequest headers (lowercase keys)
request.queryParamsmapQuery parameters
request.clientIpstringClient IP address
request.body.rawstringRaw request body (up to celBodyMaxSize, default 64KB)
request.body.jsonmapParsed JSON body (only when Content-Type is application/json)
request.tls.peerCertificate.urislistURI SANs from client cert (SPIFFE IDs live here)
request.tls.peerCertificate.dnsNameslistDNS SANs from client cert
request.tls.peerCertificate.subjectstringCertificate subject DN
request.tls.peerCertificate.serialstringCertificate serial number (hex)

request.body is only present when a CEL expression in the matched route references it — zero overhead for routes that don’t inspect the body. request.tls fields are only present when the client presented a certificate via mTLS. Always guard access with has().

CEL examples

Complex role check:

{"match": {"cel": "request.headers['x-role'] in ['admin', 'superadmin'] && request.method == 'DELETE'"}}

IP-based routing:

{"match": {"cel": "request.clientIp.startsWith('10.0.')"}}

Route by JSON body content (e.g. MCP tool calls):

{"match": {"cel": "has(request.body) && has(request.body.json) && request.body.json.method == 'tools/call'"}}

Route by raw body content:

{"match": {"cel": "has(request.body) && request.body.raw.contains('CRITICAL')"}}

Match requests from a specific SPIFFE identity:

{"match": {"cel": "has(request.tls) && request.tls.peerCertificate.uris.exists(u, u == 'spiffe://cluster.local/ns/default/sa/agent')"}}

Combining matchers

All specified matchers are AND-ed together:

{
  "match": {
    "pathPrefix": "/api/v1",
    "methods": ["GET", "POST"],
    "hostnames": ["api.example.com"],
    "headers": [{"name": "X-Tenant", "value": "acme"}],
    "queryParams": [{"name": "format", "value": "json"}]
  }
}

This matches: GET or POST requests to /api/v1/* on api.example.com with header X-Tenant: acme and query param format=json.

Route priority

Routes are evaluated in this order:

  1. Exact path (path) — highest priority
  2. Longest prefix first (pathPrefix) — /api/v1/users wins over /api/v1
  3. Regex (pathRegex)
  4. Within the same path type, more specific matchers (more headers, hostnames, etc.) win