Most zero-trust writing quietly assumes a greenfield. New services, a service mesh from day one, every workload issued a cryptographic identity at birth. That world is lovely, and almost nobody lives in it.
The real enterprise is a sediment of a decade of decisions: a Java service that hasn’t been redeployed since the last reorg, an internal API that three teams depend on and no team owns, a batch job authenticating with a shared secret in an environment variable. And now, bolted onto all of it, a new kind of workload that’s stranger than any of them: an AI agent that acts on a user’s behalf, calls tools and services autonomously, and whose next move you cannot fully predict.
These look like opposite problems. They are the same problem. Zero trust was never really about trusting the code you wrote; it’s about governing the workloads you don’t control, whether that’s a legacy binary you can’t modify or an autonomous agent you can’t anticipate. I built an open-source reference implementation around exactly this, in regulated environments, and the punchline is worth writing down: the identity, policy, and delegation primitives that onboard a legacy app without changing a line of it are the same primitives that keep an AI agent inside its lane. Here’s the architecture.
Three planes, independently replaceable
The foundation is a separation most teams collapse by accident: identity, network, and policy are three different concerns, and they should fail independently.
- Identity plane: SPIFFE/SPIRE. Every workload gets a cryptographic identity (a short-TTL, auto-rotated X.509 SVID, plus a JWT SVID for request-level proofs). No shared secrets, no long-lived credentials to leak. Identity is attested from workload properties, not handed out in a config file. Crucially, “workload” here is species-agnostic: a microservice, a legacy app’s sidecar, or an AI agent process all get identity the same way.
- Network plane: service mesh mTLS. Strict peer authentication cluster-wide, default-deny authorization, L3/L4 policy. Nothing talks to anything unless both ends present a valid identity and policy allows it.
- Policy plane: OPA. Admission control at deploy time and authorization at request time, from a single policy store so the mesh and the gateway can’t drift into disagreeing.
The principle that matters: each plane is independently replaceable. Swap the mesh for Cilium or OPA for Kyverno, and the architecture doesn’t collapse, because no plane is load-bearing for another’s guarantees. A failure at the gateway doesn’t bypass the mesh; a mesh failure doesn’t bypass admission control. Defense in depth isn’t a slogan here; it’s the topology.
flowchart TB
SPIRE["<b>Identity Plane — SPIFFE / SPIRE</b><br/>issues short-TTL, auto-rotated<br/>X.509 + JWT SVIDs to every workload"]
OPA["<b>Policy Plane — OPA</b><br/>admission control + per-request authz<br/>+ agent capability policy"]
subgraph Mesh["Network Plane — Service Mesh (strict mTLS, default-deny)"]
direction LR
GW["Zero-Trust Gateway<br/>(sidecar or dedicated)"]
LEGACY["Legacy App<br/>binds 127.0.0.1 only<br/><i>unmodified</i>"]
AGENT["AI Agent<br/><i>non-deterministic workload</i>"]
SVC["Modern Service"]
GW -->|localhost| LEGACY
end
OTEL["OpenTelemetry Collector<br/>traces · OPA decision logs · SVID metrics<br/><i>OBO enrichment</i>"]
SPIRE -. SVIDs .-> GW
SPIRE -. SVIDs .-> AGENT
SPIRE -. SVIDs .-> SVC
GW <-->|mTLS| SVC
AGENT <-->|mTLS| SVC
GW -->|authz| OPA
AGENT -->|capability check| OPA
SVC -->|authz| OPA
GW -. telemetry .-> OTEL
AGENT -. telemetry .-> OTEL
SVC -. telemetry .-> OTELThe three planes fail independently. Legacy apps join via the gateway without code changes; AI agents join as first-class but policy-bounded workloads.
That’s table stakes. The interesting part is what you do with the workloads that can’t, or shouldn’t, be trusted to behave.
The onboarding gateway: zero trust without touching the app
A legacy app can’t fetch an SVID, terminate mTLS, or call OPA. So you put something in front of it that can: a gateway that is the workload’s zero-trust citizen while the app stays blissfully unaware. Two topologies cover most of the estate:
Same-pod (sidecar) topology. The gateway runs as a container in the app’s pod. The app binds only to 127.0.0.1:8080, unreachable from outside the pod. The gateway exposes the mTLS endpoint, receives its SVID via the SPIRE SDS API, enforces the OPA check, and only then proxies down to localhost. The app sees plain HTTP from loopback and never knows it’s inside a zero-trust mesh. Nothing in its code changes.
Dedicated gateway topology. One horizontally-scaled gateway fronts many legacy pods that expose no external ports at all; NetworkPolicy restricts ingress to the gateway alone. This fits a fleet of legacy services sharing gateway infrastructure rather than paying a sidecar per pod.
The choice between them is a real tradeoff (per-pod isolation and blast-radius containment versus operational economy at fleet scale), and it’s worth recording explicitly rather than defaulting into. Either way, a workload that could never speak zero trust is now a full participant, enforced from the outside in.
The harder problem: who is actually calling?
Here’s where most implementations quietly cheat. Once service A calls B calls C, the original user’s identity is usually lost at the first hop. Worse, some designs preserve it by forwarding the user’s session token downstream, so every service ends up holding a credential it has no business holding.
The better model is On-Behalf-Of (OBO) delegation. Each service has its own SPIFFE identity. When it calls downstream it doesn’t pass the user’s token; it presents a short-lived JWT SVID proving it is authorized to act, plus headers carrying delegation proof and the original principal’s identity. OPA validates the chain at every hop (depth, audience, TTL) before allowing transit.
What you get is an auditable delegation trail that reads like a sentence:
user@corp.internal → API Gateway → Orders Service → Inventory Service
Every hop is cryptographically attested; no service holds a credential it shouldn’t. When a regulator or an incident responder asks “who actually triggered this write, and through what path?”, the answer is in the trace, not in a reconstruction. Hold onto that delegation trail; it’s about to matter even more.
The same problem, wearing a new face: AI agents
An AI agent is, architecturally, the most dangerous workload in your cluster. It’s non-human, it acts on a user’s behalf, it calls tools and other services, it chains those calls several deep. And, uniquely, its behavior is non-deterministic and steerable by its input. A prompt-injected agent will try to do things its author never intended. “Trust the code” was always shaky; for an agent, it’s meaningless, because the code’s job is to decide what to do at runtime.
Which is exactly why the primitives above fit agents better than they fit anything else:
- Agent identity. An agent is just another workload, so it gets a SPIFFE SVID like any other, not a shared API key passed around a codebase. Its identity is attested and short-lived. When it calls a tool or a downstream service, mTLS and OPA apply with no special casing.
- Capability policy. This is the agent-specific piece. OPA doesn’t just answer “can this identity call that service?” It enforces what this agent is allowed to do: which tools, which data scopes, which actions. The agent’s autonomy is bounded by policy it cannot edit. A compromised or confused agent can only ever do what its capability policy permits, no matter what its context window tells it to attempt.
- Delegation chains / OBO for agents. This is where the OBO trail pays off. When a user asks an agent to act, and that agent calls a second agent, which calls a service, the chain
user → agent → sub-agent → serviceis cryptographically attested and auditable end to end, the same mechanism as the microservice chain, now spanning autonomous actors. You can answer “which agent, acting for which user, triggered this?” with a trace, not a guess. In my blueprint this is captured directly as Zero-Trust Agent Identities (ADR-0002): agent identity, capability policy, delegation chains, and OBO tokens as one coherent model.
sequenceDiagram
actor User as user@corp.internal
participant GW as API Gateway
participant Agent as AI Agent
participant OPA as OPA (capability policy)
participant Svc as Inventory Service
User->>GW: authenticated request
Note right of GW: mints JWT SVID + OBO token<br/>principal = user
GW->>Agent: call over mTLS<br/>+ OBO token + user principal
Agent->>OPA: may this agent act for this user?<br/>(capabilities + chain depth + TTL)
OPA-->>Agent: allow — scoped to permitted tools only
Agent->>Svc: tool call over mTLS<br/>+ extended OBO chain
Svc->>OPA: validate chain (audience, depth, TTL)
OPA-->>Svc: allow
Svc-->>Agent: result
Agent-->>GW: result
GW-->>User: response
Note over User,Svc: Attested end to end:<br/>user → API Gateway → AI Agent → Inventory ServiceEvery hop carries the agent’s own identity and the original user’s principal. A prompt-injected agent still can’t exceed its capability policy, and the whole chain is auditable from the trace.
The reframe is the whole point: you do not need a separate, bolted-on “AI security” product to govern agents. If your zero-trust foundation already treats identity, capability, and delegation as first-class, agents slot into it as just another workload, admittedly a stranger one. The organizations that will struggle with agent security in the next few years are the ones treating it as a net-new problem instead of the newest instance of an old one.
Making it observable, human or machine
Identity and policy are worthless if you can’t see them. OpenTelemetry runs through all three planes: traces from gateway and sidecars, structured logs from every OPA decision, metrics on SVID rotation. A collector-side step promotes the OBO delegation headers into span attributes (caller principal, full chain), so you can search authorization decisions by trace without instrumenting the workload. That works identically whether the caller is a legacy app or an agent, which means your agent’s every tool call is as auditable as any human-initiated request.
What this is really about
Zero trust is not a greenfield architecture you migrate to. It’s an enforcement layer you wrap around the workloads you don’t control. The legacy app you can’t rewrite and the AI agent you can’t predict are the same architectural problem, and they take the same answer: attested identity, policy the workload can’t edit, and delegation that preserves the originator across the whole chain.
There’s a cost, and it’s honest to name it: OBO adds a JWT generation per request and chain-validation logic that isn’t free, and capability policy for agents is only as good as the policies you actually write. You’re buying auditability and bounded autonomy with latency and operational discipline. In a regulated, high-value system, or anywhere you’re putting autonomous agents near real data, that trade is worth making.
The full blueprint is open source, with production-ready manifests for all three planes, both gateway topologies, the OBO policy logic, the agent identity and capability model, and the architecture decision records explaining why each choice was made:
→ naren-chakraview.github.io/zero-trust-blueprint · agent patterns at distributed-systems-patterns
I’d genuinely value sharp disagreement, especially on where capability policy should live for agents, and on the OBO latency tradeoff at agent-chain depth. That’s the conversation worth having.
Narendra Loganathan is a distinguished-level engineer and enterprise architect with 25 years building large-scale distributed systems, real-time data platforms, zero-trust security, and more recently agentic AI systems, including enterprise-wide security architecture at Capital One. He publishes open reference architectures at naren-chakraview.github.io.