CEL and workload claims authorization for L4 traffic
Use Common Expression Language (CEL) expressions in ztunnel authorization policies to enforce fine-grained L4 access control based on workload identity claims.
This feature requires your mesh to be installed with the Solo distribution of Istio and an Enterprise-level license for Solo Enterprise for Istio. Contact your account representative to obtain a valid license.
This feature is considered alpha in the Solo distribution of Istio 1.30. Alpha features are likely to change, are not fully tested, and are not supported for production. For more information, see Solo feature maturity.
About CEL and workload claims authorization
Solo Enterprise for Istio extends the Istio AuthorizationPolicy resource to support Common Expression Language (CEL) expressions in ztunnel, the ambient mesh Layer 4 (L4) proxy. CEL expressions give you access to workload identity claims embedded in mTLS certificates, going beyond the fixed set of attribute keys that standard when conditions support. Ztunnel enforces CEL-based authorization at L4 on every proxied connection. No waypoint is required.
To enforce workload claims at Layer 7 using an agentgateway waypoint, see Workload identity tokens (WIMSE).
When you enable workload claims in ztunnel, istiod embeds pod-level identity claims directly in each workload’s mTLS certificate. Ztunnel reads those claims at connection setup time and exposes them in a source.claims map keyed by fully-qualified dotted names. You can then write authorization rules that reference workload identity attributes and custom security annotations.
Use cases
CEL expressions with workload claims can be useful in the following example cases:
- Enforce traffic segmentation between security zones. For example, allow only workloads annotated as PCI-DSS to reach a payment service.
- Write precise policies for workloads that share a Kubernetes service account, using workload-level claims to distinguish individual workload identities.
- Enforce consistent access policies across hybrid deployments and AI agent architectures, where workloads may operate beyond Kubernetes cluster boundaries and a SPIFFE identity alone cannot provide sufficient or stable identity signals.
Available source.claims keys
The following keys are available in the source.claims map. The map is populated from identity claims that istiod embeds in each workload’s mTLS certificate when you enabled workload claims. All keys use fully-qualified, dotted names so that the same CEL expression is portable between ztunnel and an agentgateway waypoint.
| Key | Type | Description |
|---|---|---|
source.claims['istio.io.trust_domain'] | string | Workload trust domain. |
source.claims['istio.io.workload.name'] | string | Workload name. |
source.claims['istio.io.workload.namespace'] | string | Workload namespace. |
source.claims['istio.io.workload.pod'] | string | Workload pod name. Absent from the map if empty. |
source.claims['solo.io.security-claims.<key>'] | string | User-defined security claim sourced from a pod annotation prefixed solo.io.security-claims/. The forward slash in the annotation key is converted to a dot to form the map key. For example, the annotation solo.io.security-claims/zone: PCI-DSS is accessible as source.claims['solo.io.security-claims.zone']. |
Before you begin
Install an ambient mesh with the Solo distribution of Istio version 1.30 or later.
Enroll workloads in the ambient mesh by labeling their namespaces.
kubectl label namespace <namespace> istio.io/dataplane-mode=ambient
Enable workload claims
To use the source.claims map, enable workload claims in ztunnel.
Standard Istio mTLS certificates identify workloads using a SPIFFE URI. This identity is scoped to the service account level and to workloads that the mesh CA can issue certificates for. Two pods that share a service account are indistinguishable by SPIFFE identity alone, and workloads outside the mesh boundary, such as AI agents or services in hybrid deployments, cannot present a SPIFFE identity at all. When you enable workload claims, ztunnel requests per-workload certificates and istiod embeds pod-level identity claims in each certificate, including individual workload name, namespace, and pod name, as well as custom claims from pod annotations. Ztunnel reads these claims from the peer’s certificate at connection setup time and exposes all identity claims in a source.claims map keyed by fully-qualified dotted names, giving you a richer, more portable identity model for writing granular access policies.
Get the current values for your ztunnel Helm release.
helm get values ztunnel -n istio-system -o yaml > ztunnel.yaml open ztunnel.yamlAdd the
ENABLE_WORKLOAD_CLAIMSenvironment variable under theenvkey. If theenvkey does not exist, add it.env: ENABLE_WORKLOAD_CLAIMS: "true"Upgrade the ztunnel Helm release with the updated values.
helm upgrade ztunnel oci://us-docker.pkg.dev/soloio-img/istio-helm/ztunnel \ --version 1.30.3-solo \ --namespace istio-system \ -f ztunnel.yaml
Optional: Annotate workloads with custom security claims
The source.claims['istio.io.*'] entries are populated automatically from the workload’s SPIFFE identity. If your authorization policies reference source.claims['solo.io.security-claims.*'] keys, you must annotate workloads.
Istiod reads any pod annotation prefixed with solo.io.security-claims/ at certificate issuance time and includes it as a custom claim in the workload certificate. You define the claim keys by choosing annotation suffixes that match your organization’s security classification scheme. The following example uses zone and jurisdiction, but any suffix is valid.
apiVersion: v1
kind: Pod
metadata:
name: payment-service
namespace: shop
annotations:
solo.io.security-claims/zone: PCI-DSS # accessible in CEL as source.claims['solo.io.security-claims.zone']
solo.io.security-claims/jurisdiction: US # accessible in CEL as source.claims['solo.io.security-claims.jurisdiction']
spec:
...solo.io.security-claims/ annotation, the new claim value takes effect only when the workload’s certificate is next renewed by istiod.Istiod enforces the following limits on security annotation content. Annotations that exceed these limits or contain non-printable characters are silently excluded from the workload certificate.
- Annotation key suffix: maximum 255 characters
- Annotation value: maximum 8192 characters
Write workload claims-based authorization policies
Use source.claims map keys as the when.key expression in your authorization policies. The values and notValues fields contain the strings to match against the claim’s value.
Missing key behavior
When a source workload has no solo.io.security-claims/ annotations, the source.claims map contains no solo.io.security-claims.* keys. Accessing a key that does not exist raises a CEL runtime error. Ztunnel treats CEL evaluation errors as fail-closed: for ALLOW policies the rule does not match, and for DENY policies the rule fires. Either way, an evaluation error cannot let traffic through, and traffic is denied regardless of the policy action.
For DENY policies with values, unannotated workloads are also denied. They do not match the intended condition, but the missing-key error triggers fail-closed, which fires the DENY. Apply DENY policies with claim conditions only when all source workloads in scope carry the annotation. When that is not the case, prefer an ALLOW policy that explicitly grants access to annotated workloads, so that unannotated workloads are denied by the absence of a matching ALLOW rule rather than by a CEL error.
Example: Allow traffic only from PCI-DSS workloads
The following policy allows requests to the payment-api workloads only from source workloads annotated with solo.io.security-claims/zone: PCI-DSS.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: allow-pci-zone-only
namespace: shop
spec:
selector:
matchLabels:
app: payment-api
action: ALLOW
rules:
- when:
- key: "source.claims['solo.io.security-claims.zone']"
values:
- "PCI-DSS"Example: Allow traffic only from a specific workload namespace
The following policy allows traffic to workloads in the cart namespace only from source workloads whose embedded claims identify their namespace as shop. Use source.claims['istio.io.workload.namespace'] when multiple workloads share a Kubernetes service account and source.namespace alone is not specific enough to distinguish them.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: allow-shop-workloads
namespace: cart
spec:
action: ALLOW
rules:
- when:
- key: "source.claims['istio.io.workload.namespace']"
values:
- "shop"Example: Deny traffic based on a negated claim
The following policy denies traffic from source workloads that do not have the solo.io.security-claims/env: prod annotation, which populates source.claims['solo.io.security-claims.env'] with the value prod. Because unannotated workloads trigger fail-closed behavior on the DENY rule, this pattern works correctly only when every source workload in scope carries the solo.io.security-claims/env annotation.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-non-prod
namespace: shop
spec:
action: DENY
rules:
- when:
- key: "source.claims['solo.io.security-claims.env']"
notValues:
- "prod"Considerations
Keep in mind the following considerations when deploying workload claims authorization policies in your environment.
Envoy-based proxies not supported: CEL workload claims authorization is enforced by ztunnel only. Sidecars, community waypoints (Envoy-based), and Envoy-based ingress/egress gateways do not support source.claims evaluation.
Multicluster deployments: Avoid IP-based when conditions (source.ip, destination.ip) in a multicluster ambient mesh. Source and destination IP addresses are not stable across cluster boundaries.
Workload claims require ztunnel restart: ENABLE_WORKLOAD_CLAIMS is a ztunnel configuration flag. If you set it outside of a Helm upgrade, such as by patching the environment variable directly, you must restart ztunnel manually for the change to take effect. A Helm upgrade handles the restart automatically.
kubectl rollout restart daemonset/ztunnel -n istio-systemClaims are per hop: Ztunnel evaluates claims against the immediate mTLS peer, not the original source workload. If a waypoint is in the traffic path, the destination ztunnel sees the waypoint’s claims, not the source workload’s claims. Policy that references source.claims applies based on the identity of the proximate peer at each hop.
Annotation changes take effect at certificate renewal: Workload claims reflect the annotations present at the time istiod issued the certificate. Updating a solo.io.security-claims/ annotation does not immediately affect policy evaluation. The new value takes effect when the certificate expires and istiod issues a new one. To apply annotation changes sooner, restart ztunnel. This causes ztunnel to fetch a new certificate and establish new connections, propagating the updated claims immediately.
kubectl rollout restart daemonset/ztunnel -n istio-systemEmpty workload claims: When a source workload’s certificate carries no embedded claims, for example because the peer’s ztunnel was not yet upgraded with ENABLE_WORKLOAD_CLAIMS: "true", the source.claims map is empty. Write expressions defensively to handle this case.
CEL evaluation errors are logged at debug level: When a CEL expression raises a runtime error, such as a missing key in source.claims, ztunnel logs it at debug rather than warn. In a mixed cluster where some workloads carry claims and others do not, missing-key errors occur on every connection to an unannotated workload. Logging at debug prevents those per-connection entries from masking genuine issues.