My IAM thoughts about Agentic AI – Part 2

In Part 1 of this series, we explored why Agentic AI breaks the identity model we spent the last decade building: agents have no native identity, delegation chains collapse, and static credentials don’t fit dynamic behaviour. The promise was that in Part 2 we will stop talking and start building.

So this is the practical half. We took the theory from Part 1, RFC 8693 token exchange, a Security Token Service, Workload Identity Federation, SPIFFE and turned it into a working demo. Then I built it twice, on purpose, because the question I kept coming back to wasn’t “can we make it work?” It was “how few secrets can we get away with storing?”

Let’s start and explain what I mean, and then walk through the two versions.

The same scenario, made real

I kept the same story from Part 1. A citizen, let’s call him Adam, submits a housing benefit claim through a government portal. Three AI agents process it end to end, and no human caseworker reviews the decision:

  • An Orchestration Agent receives the claim and decides what has to be verified.
  • A Verification Agent cross-checks the national ID, pulls income from the tax authority, and confirms residency with the municipal registry.
  • An Approval Agent reviews the verdict, approves the claim, and triggers the payment.

When no human sits in the loop, identity and accountability can’t be a comment in a log file. They have to be cryptographic and complete at every hop. The demo tries to prove two things at once, and it’s worth separating them clearly, because they’re easy to mush together:

Layer 1 – Workload Identity. “Is this workload even allowed to talk to the model provider?” This is the workload authenticating itself to Anthropic, and it’s where the two versions differ.

Layer 2 – Delegation Identity. “Which agent delegated to which, and on whose behalf?” This is the RFC 8693 act chain from Part 1, citizen → orchestration → verification → approval, nested inside every token. This part is identical in both versions, because the delegation story is independent of how the workload proved itself.

That split is worth holding on to. It’s the reason I built two versions: Layer 2 never changes, and Layer 1 is where the question lives.

Why two versions?

In Part 1 we stated that “long-lived, broad credentials are a security disaster waiting to happen” and that workload identity (SPIFFE/SPIRE) is “the cryptographic root everything else is built on.” That’s easy to write in a blog post. It’s harder to feel until you watch a credential actually disappear.

So I built a deliberate progression (with the help of claude code). Think of it as peeling away secrets one layer at a time:

  • v1 answers how the workload authenticates to Anthropic, and removes the static sk-ant-... API key. But it still holds one secret: an OAuth client secret.
  • v2 removes that last secret too. The workload authenticates with a SPIFFE identity it doesn’t store anywhere, it’s proven by what the workload is, not by what it knows.

The point I’d ask you to keep in mind: the Anthropic-side token exchange and the agent-to-agent delegation are the same in both versions. The only thing that changes is the source of the workload’s very first identity.

A quick word on Anthropic Workload Identity Federation

Both versions lean on a capability that made this demo possible in the first place: Workload Identity Federation (WIF) on the Anthropic platform.

The fragile way we’d normally authenticate to a model API: generate a long-lived API key (sk-ant-...), paste it into an environment variable, and hope it never leaks. That key is a bearer secret. It doesn’t expire on its own. It’s exactly the kind of standing credential Part 1 warned about.

WIF flips this around. Instead of storing a permanent key, your workload shows up holding an external JWT issued by an identity provider (like Idira) you’ve registered as trusted. Anthropic then does three things:

  1. Validates the JWT’s signature against a federation issuer you registered.
  2. Matches the JWT against a federation rule, a policy that says “a token from issuer X with subject Y is allowed to act as service account Z.”
  3. Hands back a short-lived access token (sk-ant-oat01-...) that expires in 10 min (or less, depending on your configuration).

The mechanism on the wire is RFC 7523, the JWT-bearer grant: grant_type = urn:ietf:params:oauth:grant-type:jwt-bearer, with your external JWT dropped into the assertion field. You trade a JWT you already hold for a fresh, short-lived token scoped to your service account.

This is the hinge of the whole demo. Once you have a way to exchange any trusted JWT for an Anthropic token, the only question left is: where does that first JWT come from, and how much of a secret do you have to keep to get it? v1 and v2 are two answers to exactly that question.

v1 – OAuth2 federation: no API key but it keeps one secret !

In v1 the trusted JWT comes from Idira, an Idira tenant acting as the Oauth provider. The workload runs an OAuth client credentials grant: a machine-to-machine flow. It sends its client_id and client_secret over HTTP Basic auth, and Idira returns a signed JWT.

The passport analogy holds up well here. Idira is the passport office. The JWT is a passport with an expiry date, issued to the workload only after it proves itself with credentials. The workload then presents that passport to Anthropic (RFC 7523), and Anthropic issues the short-lived token everything else runs on.

The full chain looks like this:

Idira
   │  client credentials grant (client_id + client_secret)
   ▼
OAuth JWT   sub: <client-id>   iss: https://tenantID.id.cyberark.cloud
   │  presented to Anthropic (RFC 7523 jwt-bearer)
   ▼
Anthropic WIF token  (sk-ant-oat01-..., 10 min / 600 sec)
   │  authenticates every Agent call
   ▼
Orchestration ──[STS, RFC 8693]──> Verification ──[STS, RFC 8693]──> Approval
   │
   ▼
Audit trail: Idira --> Workload --> Citizen --> Agent --> Agent  (all cryptographic)

The heart of v1 is one small module that does two steps. Step one runs the client credentials grant against Idira and gets back the JWT (id_token). Step two performs the RFC 7523 exchange against Anthropic and gets back the sk-ant-oat01-... token. From there, the three agents make their Claude calls with a Bearer token, not an x-api-key header, and every agent-to-agent hop runs through the STS to grow the act chain.

What v1 wins: there is no static sk-ant-... API key anywhere. The workload’s Anthropic credential is minted on demand and dies in an 10 minutes. If it leaks, it’s near-worthless within 10 minutes. Idira act as an OAuth server, so we can control and audit the usage of the client id and client secret.

What v1 still carries: the client secret. It lives in a .env file. You have to store it, protect it, and rotate it. In production it belongs in a secrets manager (like Idira Secret Manager – conjur for the very old 🙂), but it’s still a thing you know, which means it’s still a thing that can leak. We traded a permanent API key for a permanent client secret. That’s progress, but it isn’t the finish line.

If you’ve ever chased the secret zero problem, the credential you need to bootstrap all your other credentials, you can probably already see where this is heading.

v2 – SPIFFE: remove the last secret

v2 asks the obvious follow-up: what if the workload didn’t have to know anything to prove who it is? What if its identity came from what it is?

That’s what SPIFFE gives us. SPIFFE (Secure Production Identity Framework For Everyone) is an open standard for giving workloads unique and verifiable identities. A SPIFFE ID looks like a URL:

spiffe://sme-access.com/workload/benefit-processing
        └─ trust domain ┘ └──── workload path ────┘

Package that identity as a signed JWT and you get a JWT-SVID, which is exactly the kind of token Anthropic’s WIF already knows how to consume. Same RFC 7523 exchange, different source.

Note: the demo uses JWT-SVID. X.509-SVID exists and serves a different set of use cases.

SPIRE is the reference implementation. It has two parts, and the analogy from before extends cleanly:

  • The SPIRE Server is the certificate authority and passport office. It holds the signing keys and the registry of who’s allowed to be whom.
  • The SPIRE Agent is the passport officer at the door. It attests the workload, proves what it is, and hands it an SVID (SPIFFE Verifiable Identity Document) through a local Unix socket.

The key word is attestation, and it’s the thing that replaces the secret. How does the agent know the process asking for an identity really is benefit-processing and not an impostor? In this demo we use the Docker workload attestor: the agent inspects the calling process, traces it to its container, reads the container’s label (app=benefit-processing), and matches it against a registration entry. No secret is exchanged. The identity is proven by what the container is. There’s no client ID, no client secret, no API key stored anywhere.

The chain in v2:

SPIRE Server  (trust domain: sme-access.com)
   │  signs (no secret held by the workload)
   ▼
JWT-SVID   sub: spiffe://sme-access.com/workload/benefit-processing
   │       iss: https://spire.sme-access.com
   │  presented to Anthropic (RFC 7523 jwt-bearer)
   ▼
Anthropic WIF token  (sk-ant-oat01-..., 10 min)
   │  authenticates every Claude call
   ▼
Orchestration ──[STS, RFC 8693]──> Verification ──[STS, RFC 8693]──> Approval
   │
   ▼
Audit trail: SPIRE --> Workload --> Citizen --> Agent --> Agent  (all cryptographic)

One implementation detail caught me out, so it’s worth flagging. v1’s Idira tenant is on the public internet, so Anthropic can auto-discover its keys via OIDC discovery. The SPIRE Server in v2 runs locally and isn’t reachable from the internet, so Anthropic can’t go fetch its keys. The fix is to register the issuer with an inline JWKS: you export the SPIRE Server’s public keys and paste them directly into the Anthropic Console. Verification still works the same way, you just hand over the keys manually instead of pointing at a discovery URL.

The code reflects how little actually changes between the two. v2’s auth module tries SPIFFE first, and if the SPIRE socket isn’t present, it falls back to the v1 OAuth path.

What this looks like running

Both demos run in two phases. Phase 0 is the workload identity dance, the workload gets its JWT (from Idira in v1, from SPIRE in v2) and exchanges it for the Anthropic token. Phase 1 is the claim processing, where the three agents resolve Adam’s claim and each hop grows the act chain through the STS.

In v1, Phase 0 prints the Idira JWT arriving with its sub and iss, then the Anthropic Bearer token being minted.

In v2, the same step instead shows a SPIFFE JWT-SVID with sub: spiffe://sme-access.com/workload/benefit-processing, fetched with no secret.

Everything after that, the orchestration → verification → approval flow and the final audit trail, is indistinguishable between the two. That’s the proof we were after: swap the root of identity, and the rest of the system doesn’t even notice.

The audit trail at the end reads like the chain of authorization letters from Part 1: citizen → orchestration → verification → approval, every hop cryptographically recorded, no caseworker involved, and full provenance preserved.

Running it yourself

I’ve deliberately kept this post at the level of the ideas, what each version proves and why the secret keeps shrinking, rather than turning it into a setup manual. The full step-by-step configuration for both versions lives in the README files in the GitHub repositories: how to set up the Idira tenant and OAuth2 client for v1, how to bootstrap the SPIRE Server and Agent for v2, how to register the federation issuer and rule in the Anthropic Console, and how to run each demo end to end.

Where to go from here

If Part 1 was the “why,” this was the “how,” twice over, so the cost of each secret you remove is visible. We’d suggest starting where Part 1 pointed: inventory your agents, establish workload identity, route every agent-to-agent call through an STS, and build provenance into your logs. v1 is a reasonable first step if you already live in an OAuth world. v2 is where it’s worth ending up, because the only secret you can't leak is the one you never had.

But let’s be precise about what Part 2 actually solved: one problem, how to issue a unique, verifiable identity to an agent using current standards. That’s it. We didn’t touch access control. We didn’t touch permissions. We didn’t ask what that agent is actually allowed to do once it has a valid token.

That’s the next frontier: Just-In-Time access and Zero Standing Privilege for agentic workloads. What does it mean to grant an agent the minimum privilege it needs, only for the duration of a specific task, with no persistent entitlements sitting around waiting to be abused?

That’s probably Part 3 😉

Both demos are open, and the READMEs walk through the full setup for each.

Thanks for reading both parts.

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *