Key takeaways: The identity plumbing for zero trust delegation is accomplished by wiring three technologies together: SPIFFE for service-to-service cryptographic workload identity (mTLS), AuthBridge via RFC 8693 token exchange to pass user delegation context (JWTs), and Kagenti for agent lifecycle management and policy binding. These components authenticate and authorize every request according to the permission intersection pattern.
The autonomy of AI agents necessitates a zero trust architecture, but zero trust for AI agents introduces different challenges from zero trust for human users. In Part 1 of this series on implementing zero trust for AI agents on Red Hat OpenShift, “Zero trust for AI agents: why delegation beats impersonation,” we introduced the permission intersection pattern—agents can only reduce user permissions, never expand them. But we treated identity as a given: users have identities, agents have identities, and the policy engine checks them. This post looks underneath that abstraction.
How does a service prove it is who it claims to be? How does a user’s identity survive the hop from browser to agent to document service? And how do AI agents get discovered and registered in the system in the first place?
These are infrastructure questions, and the answers involve three technologies working together: SPIFFE for workload identity, RFC 8693 token exchange for delegation across trust boundaries, and Kagenti (an open source project for agent orchestration) for agent lifecycle management on OpenShift. Each solves a distinct problem, but they need to be wired together carefully, especially when AI agents are in the mix.
Note: Red Hat’s Emerging Technologies blog includes posts that discuss technologies that are under active development in upstream open source communities and at Red Hat. We believe in sharing early and often the things we’re working on, but we want to note that unless otherwise stated the technologies and how-tos shared here aren’t part of supported products, nor promised to be in the future.
Workload identity with SPIFFE
In a zero trust architecture, network location means nothing. A service running inside the cluster doesn’t get trusted just because it’s “internal.” Every service-to-service call needs cryptographic proof of identity.
SPIFFE (Secure Production Identity Framework for Everyone) provides this through X.509-SVIDs:short-lived certificates issued by a SPIRE server that encode a service’s identity as a URI.
spiffe://demo.example.com/service/agent-service
spiffe://demo.example.com/service/document-service
spiffe://demo.example.com/service/opa-service
These certificates enable mTLS between services. When the agent-service calls the document-service, both sides present their SVIDs and verify each other’s identity before any data flows. No shared secrets, no API keys, no long-lived credentials—just short-lived certificates that auto-rotate (1-hour TTL in our demo).
There’s an important distinction in our architecture: SPIFFE identities operate at two different layers.
At the transport layer, SPIRE issues X.509 certificates to services—the long-running processes that handle requests. These certificates enable mTLS between services. At the application layer, users and agents are identified by logical identifiers carried inside JWT tokens.
Transport layer (mTLS): WHO is calling? → Service identity (X.509-SVID)
Application layer (JWT): ON BEHALF of whom? → User + agent delegation chain
A concrete example makes this clearer. When Alice delegates to summarizer-tech to access a document, here’s what identity looks like at each hop:
| Hop | mTLS identity (transport) | JWT payload (application) |
|---|---|---|
| Browser → User Service | TLS (no SVID) | OIDC token: alice |
| User Service → Agent Service | spiffe://.../service/user-service | Delegation: alice → summarizer-tech |
| Agent Service → Document Service | spiffe://.../service/agent-service | JWT with sub: summarizer-tech, act: {sub: alice} |
| Document Service → OPA Service | spiffe://…/service/document-service | Policy query: user=alice, agent=summarizer-tech |
The mTLS column answers “which service is calling?” The JWT column answers “on whose behalf?” Both are verified independently—a compromised service can’t forge the JWT, and a stolen JWT can’t bypass mTLS.
Why not issue per-user SPIFFE certificates instead? Scaling. SPIRE manages certificates for long-lived workloads that register at deploy time. Users come and go through OIDC login sessions; tracking each one as a SPIRE registration entry doesn’t fit the model. The two-layer approach lets each technology do what it’s best at: SPIFFE secures infrastructure, JWTs carry delegation context.
For Kagenti-deployed agents, the operator can bind real SPIFFE identities via the AgentCard CR—giving agents their own cryptographic identity when the deployment model supports it.
Token exchange with AuthBridge
The two-layer identity model creates a practical problem: how does a user’s OIDC token from Keycloak become a scoped JWT with delegation context that the document-service can verify?
This is where AuthBridge comes in—an Envoy-based sidecar that intercepts requests to the agent-service and performs RFC 8693 token exchange. The flow works like this:
- Alice logs in through Keycloak and gets an OIDC access token.
- She delegates to
summarizer-tech—her request hits the agent-service. - Envoy’s ext-proc (external processing) filter intercepts the request before it reaches the agent-service.
- The ext-proc calls Keycloak’s token exchange endpoint, swapping Alice’s OIDC token for a new JWT that includes the delegation context—the nested act claim from Part 1.
- The agent-service receives the enriched JWT and forwards it to the document-service.
- The document-service validates the JWT signature against Keycloak’s JWKS (JSON Web Key Set) endpoint and extracts the delegation chain.
Alice's OIDC token Delegated JWT
┌──────────────────┐ ext-proc ┌──────────────────────────┐
│ sub: alice │───────────────▶ │ sub: summarizer-tech │
│ iss: keycloak │ RFC 8693 │ act: │
│ scope: openid │ exchange │ sub: alice │
└──────────────────┘ │ departments: [eng,fin] │
└──────────────────────────┘
The key design choice is putting this in the infrastructure layer, not the application. The agent-service code doesn’t perform token exchange—Envoy handles it transparently. This is exactly what the sidecar pattern was designed for: offloading cross-cutting concerns like authentication so that application services don’t have to implement them. Any service behind the Envoy sidecar gets token exchange for free, without writing a single line of authentication logic.
AuthBridge is built on a stack of RFCs that make each piece interoperable:
- RFC 8693—defines the token exchange protocol
- RFC 7519—JWT format for the exchanged tokens
- RFC 8705—mutual TLS client authentication (SPIFFE SVIDs as client certs)
- RFC 9068—standardized JWT structure for access tokens
This standards-based approach matters. We’re not inventing a custom token format—any service that can validate a JWT can participate in the delegation chain.
From agent discovery to policy binding
The identity and token exchange layers deliver security capabilities that handle how requests flow between services. But there’s a lifecycle question they don’t answer: how do AI agents get registered in the system, and how do their names map to actual permissions?
In the A2A protocol, every compatible agent exposes its agent card at .well-known/agent-card.json—a machine-readable description of the agent’s name, endpoint, and capabilities. When you deploy an A2A agent on OpenShift, the Kagenti operator discovers it and automatically creates a Kubernetes custom resource (CR) called AgentCard.You don’t create AgentCards manually—Kagenti handles that. Our agent-service acts as an agent gateway: it watches for AgentCard CRs, maintains a list of available agents, and surfaces them to the web dashboard where users can pick an agent from a menu.
In our demo, we use a {function}-{scope} naming convention: summarizer-hr, summarizer-tech, reviewer-ops. The same container image—a generic summarizer or reviewer—gets deployed multiple times under different names. This isn’t part of any product or specification—it’s just the convention we found useful for this demo. The name signals to users what the agent is for: if you need an HR document summarized, summarizer-hr is the obvious choice. Your naming scheme may look entirely different.
But whatever naming convention you choose, the name is a convention for humans, not a permission grant. Deploying an agent called summarizer-hr doesn’t automatically give it access to HR documents. That binding happens in the policy engine. In our OPA implementation, the Rego policy explicitly maps agent names to capability sets:
agent_capabilities["summarizer-hr"] := ["hr"]
agent_capabilities["summarizer-tech"] := ["finance", "engineering"]
agent_capabilities["reviewer-ops"] := ["engineering", "admin"]
This separation is deliberate. The naming convention makes agent selection intuitive—users don’t need to read policy files to choose the right agent. But the policy engine is the single source of truth for what an agent can actually do. If someone deploys a summarizer-everything agent, it has zero capabilities until a policy explicitly grants them.
The combination of Kagenti discovery and policy binding gives you a deploy-then-authorize workflow:
- Deploy an A2A-compatible agent on OpenShift.
- Kagenti discovers it and creates an AgentCard CR.
- The agent gateway picks it up and makes it available to users.
- Define the agent’s capabilities in the policy engine.
- The permission intersection pattern from Part 1 takes over—the agent can only access what both the user and the policy allow.
This also makes capability changes safe. You can narrow an agent’s scope by updating a single policy rule without redeploying the agent. The agent itself never knows the difference—it stays auth-unaware.
What’s next
Three technologies, each solving a distinct problem. SPIFFE proves which service is calling at the transport layer. Token exchange via AuthBridge carries on whose behalf at the application layer. And Kagenti handles the agent lifecycle—from discovery to policy binding.
None of these are custom inventions. SPIFFE is a CNCF (Cloud Native Computing Foundation) graduated project. Token exchange follows RFC 8693. Envoy’s ext-proc is a standard extension point. Kagenti builds on Kubernetes custom resources. The value isn’t in any single piece—it’s in composing them into a coherent identity flow where every hop is authenticated, every delegation is traceable, and every agent is authorized through the permission intersection pattern from Part 1.
But there’s a gap this architecture doesn’t close yet. Once an agent has a verified, scoped JWT, it needs to use it to access real resources—an S3 bucket, a GitHub repo, a Slack workspace. Each of these speaks a different authentication language. Translating a Zero Trust delegation token into resource-specific credentials is the “last mile” problem, and it’s harder than it sounds.
In Part 3, I’ll walk through how we built a credential gateway that bridges this gap—starting with AWS S3 and STS session policies that enforce permission intersection natively.
This post is part of a series on implementing Zero Trust for AI agents on OpenShift:
- Part 1: Why delegation beats impersonation
- Part 2: Wiring Zero Trust identity—SPIFFE, token exchange, and Kagenti (this post)
- Part 3: The last mile—from Zero Trust tokens to real-world resources (coming soon)
More documentation is available at https://github.com/redhat-et/zero-trust-agent-demo/blob/main/docs/README.md#reading-guide-for-the-blog-series.

