Introduction to CEL¶
The Basic Syntax¶
Using CEL in Kuadrant, you evaluate the Request (attributes like path, method, headers) or the Connection (mTLS details, source IP).
Variables and Attribute Access¶
Kuadrant exposes a standard set of attributes. You access them using dot notation or map lookups.
- Dot Notation:
request.path,request.method - Map Lookup:
request.headers['user-agent'](Required for headers, as they contain hyphens).
Literals¶
CEL supports standard data types:
| Type | Examples |
|---|---|
| Int | 200, 404, -1 |
| UInt | 200u, 404u |
| String | 'GET', "/api/v1", r"regex\d+" |
| Bool | true, false |
| Duration | duration('500ms'), duration('10s') |
| Map | {'group': 'admin', 'tier': 'gold'} |
| List | [1, 2, 3] |
For more details on all member overloads for these types, please refer to the standard library docs.
[!Caution] CEL has no implicit type coercion. It isn't valid to for example compare different types, this expression is invalid:
"3" != 3
Logical Operators¶
Within policies, in Predicates you can combine checks. If the expression evaluates to true, the policy applies (e.g. allowing or denying the request based on the action).
Boolean Logic¶
-
AND (
&&): Both conditions must be true. -
OR (
||): At least one condition must be true. -
NOT (
!): Inverts the result.
Conditional Logic (If/Else)¶
Conditional logic is useful for dependent checks, such as validating specific headers only for certain paths.
// If path is /secure, check for x-user-id header, otherwise allow.
request.path.startsWith('/secure') ? has(request.headers['x-user-id']) : true
Handling Optional Fields (Presence)¶
In HTTP traffic, headers and metadata are often missing. Accessing a missing map key in CEL can result in an error or no_such_field.
The has() Macro¶
Use has() to check if a header or metadata field exists before accessing it.
// Rule: If an Authorization header exists, it must start with 'Bearer'
has(request.headers['authorization']) ? request.headers['authorization'].startsWith('Bearer ') : true
[!Note] For
request.headers, checkinghas()ensures the key exists in the map. For standard attributes likerequest.referer, it checks if the value is populated. See below to learn about the optional syntax, which can in places be an alternative to the ternary operator.
Working with Lists and Maps¶
While standard HTTP headers are often strings, Kuadrant provides powerful lists in some contexts like JWT Auth (Claims).
In CEL, you can use these macros on both Maps or Lists to work with collections:
.all()¶
Checks if every item in the list satisfies a condition.
.exists()¶
Checks if at least one item satisfies a condition.
// Rule: The JWT 'groups' claim must contain 'admin'
auth.identity.claims['groups'].exists(g, g == 'admin')
.exists_one()¶
Checks if exactly one item satisfies the condition.
.filter()¶
Returns a new list containing only the elements that satisfy the condition.
// Get all groups that end with '.admin'
auth.identity.groups.filter(group, group.endsWith('.admin'))
.map()¶
Returns a new list where each element has been transformed by the expression.
String Manipulation & Regex¶
Validating paths and headers.
Comparisons¶
- Equality:
request.method == 'PUT' -
Prefix/Suffix:
-
Contains:
request.headers['user-agent'].contains('Mozilla')
Regular Expressions¶
CEL uses RE2 syntax for regex.
// Rule: X-Request-ID must be a UUID-like format
request.headers['x-request-id'].matches(r'^[0-9a-f-]+$')
[!Tip] Always use
r'...'for regex strings to handle backslashes correctly.
Type Conversion & Math¶
HTTP headers are always strings. To compare them numerically (e.g., Content-Length or custom logic), you must cast them.
Casting¶
- int(): Converts strings to integers.
- size(): Returns the size of a string, list, or map.
// Rule: Content-Length must be less than 1MB (1,000,000 bytes)
has(request.headers['content-length']) && int(request.headers['content-length']) < 1000000
Timestamps and Durations¶
You can create timestamp & duration values, using these functions to operate on these types:
// Was the request made in the first 12 hours of 2025?
request.time - timestamp('2025-01-01T12:00:00Z') < duration('12h')
For more details on all member overloads for these types, please refer to the standard library docs.
The Optional Type¶
The optional type offers a cleaner way to handle missing headers or metadata without verbose has() checks.
Creating Optionals¶
You can wrap values that might be missing:
optional.of(value): Wraps a value.optional.none(): Represents a missing value.
Unwrapping with Defaults (orValue)¶
Provide a default value if the header is missing.
Old Way (Verbose):
New Way (Optional):
// If header is missing, default to 0, then check if < 3
optional.of(request.headers['x-retries']).orValue('0').matches(r'^[0-2]$')
(Note: Since headers are strings, we handle the value as a string or cast inside a map).
Optional syntax:
The .? operator will not err out if the field, headers in this case, isn't present. Instead it will return a
Optional representing None. If on the other hand the field is there, in this case a Map<String, String>, the
value will be wrapped into a Optional holding the actual reference to the value.
The [?<index>] syntax does the same for index accesses into a collection, whether it's a List or a Map.
To use the value, access it using .orValue() providing a default value in the case of absence.
To read more about the Optional type, see the Optional documentation.
Safe Transformation (optMap)¶
Transform a value only if it exists.
// Rule: If 'x-debug' header exists, it must be 'true'. If missing, pass.
optional.of(request.headers['x-debug'])
.optMap(val, val == 'true')
.orValue(true)
Chaining (or)¶
Check multiple headers in order of preference.
// Use 'x-client-id', or fallback to 'x-app-id', or default to 'anonymous'
optional.of(request.headers['x-client-id'])
.or(optional.of(request.headers['x-app-id']))
.orValue('anonymous')
Summary Cheat Sheet¶
| Requirement | CEL Expression Strategy |
|---|---|
| Header must exist | has(request.headers['x-token']) |
| Path validation | request.path.startsWith('/api/') |
| Regex Match | request.headers['authority'].matches(r'.*\.internal$') |
| Size Limit | request.size < 1024 |
| Header Fallback | request.headers[?'x-group'].orValue('guest') |