The Cerbos PDP v0.55.0 release is about making policy evaluation more predictable. A new strict evaluation mode turns silent runtime errors in conditions into explicit denials, auxData can now carry several named JWTs instead of one, and policy conditions are simplified at compile time so they cost less to evaluate on every request. The release also upgrades the underlying CEL library, which adds 21 new expression functions and changes some behaviour worth checking before you roll it out. For full details, see the release notes.
Strict evaluation mode
We flagged this one in the v0.54.0 post. Until now, a condition that raised a runtime error, typically an unknown field access or a type mismatch, was treated as unsatisfied and logged. The rule simply did not match.
For an ALLOW rule that behaviour is safe, because a rule that fails to grant access denies by default. For a DENY rule it is not. The rule that was supposed to block the request never matches, evaluation continues as though it were not there, and a broken expression quietly widens access instead of narrowing it.
Strict evaluation mode changes what a runtime error means. When a condition or variable expression raises one, the affected action is denied rather than skipped. The scope of that denial is deliberately narrow. An error in a variable denies only the actions whose conditions reference that variable. An error in the condition of an imported derived role denies the actions of rules that name it in their derivedRoles, along with any actions whose conditions read runtime.effectiveDerivedRoles. Everything else in the policy evaluates as normal. The mode applies to both the check and the query planner APIs.
It is opt-in and off by default, so upgrading changes nothing until you turn it on in your engine configuration.
engine:
strictEvaluation: true
Before enabling it against live traffic, run your test suites under the same rules with the new flag on cerbos compile.
cerbos compile --strict-evaluation /path/to/policies
That surfaces the expressions that would start denying while the consequences are still limited to a red test. If you enabled CEL error logging in v0.54.0 via engine.celErrorLogLevel, those log entries are already a working list of the expressions to fix.
Multiple JWTs in auxData
auxData has always accepted a single JWT, which works when one token carries everything a policy needs. It gets awkward as soon as a request crosses more than one identity boundary. A gateway token identifying the calling service and an end user token identifying the person the call is made on behalf of are both relevant to the decision, and only one of them could be attached. The usual workaround was flattening the second token's claims into principal attributes, which throws away the verification the PDP does on the token itself.

The auxData field now accepts a jwts object holding a set of named tokens, each with its own keyset. A CheckResources request carrying both tokens looks like this.
{
"requestId": "0ba0f0c7-b0a6-4b4c-9c9a-1f1c5f3a2b11",
"principal": {
"id": "alice@example.com",
"roles": ["manager"]
},
"resources": [
{
"actions": ["approve"],
"resource": {
"kind": "expense",
"id": "XX125",
"attr": {
"department": "finance",
"amount": 4200
}
}
}
],
"auxData": {
"jwts": {
"gateway_token": {
"token": "aaa.bbb.ccc",
"keySetId": "gateway_keys"
},
"user_token": {
"token": "xxx.yyy.zzz",
"keySetId": "workforce_idp"
}
}
}
}
Claims from each token are addressed by name in policy conditions using request.auxData.jwts.NAME.claims.CLAIM, so a rule can require agreement between the two instead of trusting either in isolation.
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: expense
rules:
- actions: ["approve"]
effect: EFFECT_ALLOW
roles: ["manager"]
condition:
match:
all:
of:
# The call arrived through the internal gateway, not directly.
- expr: request.auxData.jwts.gateway_token.claims.iss == "internal-gateway"
# The end user is in the same department as the expense.
- expr: request.auxData.jwts.user_token.claims.department == request.resource.attr.department
# The user token was issued with the scope this action needs.
- expr: "expense:approve" in request.auxData.jwts.user_token.claims.scopes
The service identity and the user identity stay separate all the way into the rule, so a policy can say something a single flattened token cannot, which is that this specific user is allowed to approve this expense and that the request reached the PDP through an approved path.
The jwts field is mutually exclusive with the singular jwt field, and server side validation settings are unchanged and apply to both. Keysets and verification rules you already have configured carry over as they are.
Constant folding for policy conditions
Policy conditions now go through a constant folding step during compilation. Sub-expressions whose inputs are known before any request arrives are evaluated once at compile time and replaced with their result, so that work no longer repeats on every check. Policies that build lists from literals, parse fixed timestamps, or compile regular expressions written inline benefit most, and in some cases the reduction in evaluation time is substantial.
The same step catches a class of mistake earlier. Invalid time and regex definitions that previously surfaced as a runtime error on a live request are now rejected at compile time, which is a useful pairing with strict evaluation mode. Fewer expressions can fail at request time in the first place.
21 new expression functions
Network conditions get proper support. ip() and cidr() parse values, isIP() and isCIDR() validate them, and methods including containsIP(), containsCIDR(), masked(), prefixLength() and isMask() let you reason about ranges directly instead of doing string prefix matching and hoping the format holds.
- expr: cidr("10.0.0.0/8").containsIP(ip(request.principal.attr.ipAddress))
Address classification is covered too, with family(), isLoopback(), isUnspecified(), isGlobalUnicast(), isLinkLocalUnicast() and isLinkLocalMulticast().
Beyond networking, regex.extractAll() and regex.replace() handle pattern extraction and rewriting, sets.contains(), sets.equivalent() and sets.intersects() cover set comparison, and json.encode() serialises a value to a JSON string.
Breaking changes and upgrade notes
The CEL upgrade is the part to plan for. v0.30.0 is stricter about unknown field access, and some query plans the PDP produces will differ from what earlier versions returned. If you consume PlanResources output and translate it into database queries, generate plans for a representative set of requests before and after the upgrade and compare them, rather than assuming they match.
As with any release that touches evaluation semantics, validate your policies and workloads in staging before promoting to production.
Bug fixes
Two fixes land in this release.
Test suites failed to load if they referenced fixtures using YAML block scalar syntax, meaning multiline strings written with | or >, in their attributes. That was a regression introduced in v0.54.0 and is resolved.
The except, hasIntersection, intersect and isSubset functions returned inconsistent results when the two lists held different numerical types, floats compared against integers, with the answer depending on the size of the lists. Comparison is now consistent regardless of list size.
Upgrade today
For the complete list of changes, see the full changelog and the v0.55.0 release notes.
Try Cerbos Hub to manage, test, and deploy policies across your PDPs, or book a call to talk through your authorization architecture with the team.
Tagged in




