Dev.to · 6 min read

SPIFFE Workload Identity for AI Agents, End to End

SPIFFE Workload Identity for AI Agents, End to End

Originally published at webofmike.com on 2026-09-02. The demo repo and every command in it were run before publishing. I built a demo where an AI agent calls a model and nothing in the path holds a certificate file. Not the agent, not the gateway, not the model upstream. Every identity is issued at runtime by SPIRE, rotates on its own, and is verified on the TLS handshake rather than read out of a header. The gateway's authorization policy is written against a SPIFFE ID. It all runs on docker compose. The code is at themsquared/agent-identity-spiffe. Yesterday I wrote about agents that hold no LLM credential, where the gateway holds the provider key and the agent authenticates with a short-lived JWT from a local issuer. That issuer was the weak part of the design. I wrote it myself, it signed whatever it was asked to sign, and the claim it put in the token (team: research) was an assertion nobody checked. This post replaces it with the real thing. A bearer token is the wrong primitive for agent identity The normal way to authenticate an agent to a gateway is a bearer token: an API key, a static JWT, something the agent presents and the gateway believes. The trouble is structural. A bearer token is a thing that can be copied, so it has to be stored, and wherever it is stored is what an attacker goes for. It lands in an environment variable, gets logged by an HTTP client with verbose tracing on, and shows up in a crash dump. The compromised dependency that reads it gets everything that token can do, for as long as the token lives. Agent workloads make this worse in two specific ways. They run a lot of third-party code by design, since the whole value proposition is calling tools and libraries on your behalf. And they are increasingly ephemeral, which means the operational pressure is toward long-lived credentials baked into an image, because nobody wants to rotate a secret across a fleet that recreates itself constantly. SPIFFE takes a different position: stop giving workloads secrets. A workload asks the local Workload API who it is, and gets back an X.509 SVID whose issuance was conditioned on attested properties of the workload itself. There is nothing to copy into a config file because nothing was ever written down. The certificate lives minutes, and renewal is a background stream rather than an operational event. What agentgateway v1.5.0 added The gateway is the part that was missing. agentgateway could already terminate mTLS, but only from a static cert and key on disk, which reintroduces exactly the file you were trying to eliminate. v1.5.0 added the Workload API as an identity source, and three things follow from it: The gateway fetches its own SVID and trust bundle from the Workload API and rotates them automatically. The same identity terminates the frontend listener and authenticates outbound connections to backends. The peer's verified SPIFFE ID is exposed to CEL policy as source.spiffeId. If SPIFFE is enabled and the socket cannot be reached, the gateway fails to start rather than serving without an identity. That default is the right one and it is worth knowing before you deploy it. The whole SPIFFE surface is three stanzas Here is the config the demo runs. There is no certificate path in it, which is the point: config: spiffe: endpoint: unix:///run/spire/sockets/agent.sock binds: - port: 3000 listeners: - name: agents protocol: HTTPS tls: spiffe: {} routes: - policies: authorization: rules: - allow: 'source.spiffeId == "spiffe://example.org/ns/demo/sa/agent-alpha"' backendAuth: key: $UPSTREAM_API_KEY backendTLS: spiffe: {} subjectAltNames: - spiffe://example.org/ns/demo/sa/mock-llm backends: - host: mock-llm:8443 tls.spiffe terminates the listener with the SVID from the Workload API. Client certificates are mandatory in this mode and are verified against the trust domain bundle, so by the time the authorization rule reads source.spiffeId, it is a verified fact and not a client assertion. backendTLS.spiffe handles the other leg. The gateway presents its own SVID to the upstream, and subjectAltNames pins which upstream identity it will accept. SVIDs carry a spiffe:// URI SAN and no DNS SAN, so ordinary hostname verification does not apply and this pin is how you narrow it. backendAuth.key attaches the provider credential outbound. That is the part the agent never sees. Identity is not authorization The demo runs two agents that differ in exactly one respect: their SPIFFE ID. Both are attested by the same SPIRE agent, both hold valid SVIDs from example.org, both complete the TLS handshake with the gateway. agent-alpha is in the CEL rule: agent: my SPIFFE ID is spiffe://example.org/ns/demo/sa/agent-alpha agent: provider credentials I hold: {'env': 'none', 'key_files': 'none'} agent: HTTP 200 agent: Upstream saw client SPIFFE ID spiffe://example.org/ns/demo/sa/agentgateway and a valid provider credential. agent-beta is not: agent: my SPIFFE ID is spiffe://example.org/ns/demo/sa/agent-beta agent: provider credentials I hold: {'env': 'none', 'key_files': 'none'} agent: HTTP 403 authorization failed Two things in the first output are worth reading carefully. The agent reports holding no provider credential, and it checks honestly: it walks its own environment for anything shaped like an API key and its own filesystem for anything shaped like key material, and finds neither. Yet it gets a 200 back. The upstream refuses to answer without the provider credential, so if a completion came back, the gateway attached one. The second is that the upstream reports seeing sa/agentgateway, not sa/agent-alpha. The gateway authenticated to the model with its own identity over its own mTLS connection. The agent's identity terminated at the gateway, which is what you want: the blast radius of a compromised agent is a 403, not a set of upstream credentials. The demo's whoami route makes the verification explicit. It is a directResponse whose body is built from a CEL expression: directResponse: status: 200 bodyExpression: '"verified client SPIFFE ID: " + source.spiffeId + "\n"' client says: spiffe://example.org/ns/demo/sa/agent-alpha gateway says: verified client SPIFFE ID: spiffe://example.org/ns/demo/sa/agent-alpha The client cannot influence the second line. There is no header to set. Watching the certificate rotate underneath a running process The demo issues five-minute SVIDs. SPIRE renews at roughly half the lifetime, and the SPIFFE client library swaps the certificate in place without the workload restarting, reconnecting, or asking: [ 0s] serial=ad928b7fcb3992f3 expires=15:58:34Z [ 30s] serial=ad928b7fcb3992f3 expires=15:58:34Z [ 60s] serial=38e419281166908e expires=16:01:02Z

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News