LLM Shield

Non-human identity, layer by layer

An agent has no password, no MFA, and nobody at the keyboard when it acts. Every control that assumes a human is present fails silently. This is what replaces them: five layers, each answering one question, each independently switchable.

core/workload_identity · core/agent_tokens · core/identity_resolution · core/delegation · core/capabilities

The five questions each layer answers exactly one

Read bottom-up. A layer is only meaningful if the one below it holds: a capability scoped to an agent means nothing if the agent principal was self-asserted.

L4Capability
May it do this specific thing, to this specific resource, right now? Not a role. A single signed grant, burned on use.
mint_cap() Ed25519
tool + resource + nonce
replay-burned in Redis
on
L3Delegation
Whose authority is it borrowing? The agent's own grant intersected with the user's. Neither can exceed its own.
X-On-Behalf-Of
RFC 8693 act claim
core/delegation.py
off by default
L2Binding
Is the presenter the party this was issued to? A stolen bearer token is a valid bearer token. Proof-of-possession is what makes theft insufficient.
DPoP RFC 9449
cnf.jkt RFC 7638
or mTLS RFC 8705
off by default
L1Agent principal
Which agent, which build, which model, which session? The identity authorization is written against. Short-lived by design.
agent token EdDSA JWT
IdentityTuple, 8 fields
≤ 15 min, revocable
on
L0Workload attestation
What process is asking, and did the platform vouch for it? The root of trust. Everything above is claims until something here is verified.
spiffe · mtls
oidc_sa (Keycloak, K8s SA)
admin_key (bootstrap)
2 of 4 enabled

Where each layer sits on the request path one guarded hop

Platform
Attestor
SPIRE, the mesh CA, or your IdP. Issues the workload credential the agent process did not choose for itself.
SPIFFE SVID
Keycloak / K8s SA JWT
client cert
Runtime
Agent process
Presents its workload credential once, gets a short-lived agent token, then mints a capability per action.
1× per process
1× per tool call
Data plane
resolve_identity()
The single seam. Verifies the workload identity, resolves the agent and role, records where each came from.
agent_source
role_source
binding · acting_for
Data plane
Authorization
Intersection: the agent's grant AND the role's grant. A capability narrows it further to one resource.
tool_allowlist.py
agent AND role
Downstream
Tool / MCP server
Receives a capability, not a role. Verifies the signature itself and burns the nonce, so it never has to trust the caller.
Ed25519 public key
nonce single-use

Lifetimes the real control, nothing here is long-lived

Human credentials last a workday because a human is slow. Agent credentials should last one action, because the agent is not. Short lifetime is what replaces the revocation you cannot deliver in time.

CredentialDefaultHard capScopeRevocation
Workload credentialissuer-setissuer-set the processthe attestor's own rotation
Agent token10 min15 min agent + instance + sessionby instance, by jti, by user
Capability30 s60 s one tool, one resourcenonce burned on first use

What is on, and what you must turn on read this before claiming coverage

ControlEnvDefaultWhat the default means
Workload providersSHIELD_WORKLOAD_IDENTITY_PROVIDERS admin_key,spiffe oidc_sa and mtls are not enabled. An OIDC token is ignored entirely until you add it.
Role bindingSHIELD_ROLE_BINDINGoff The role comes from the X-User-Role header, chosen by the caller RBAC is constraining. prefer lets a signed claim win.
Token bindingSHIELD_TOKEN_BINDINGoff Tokens are bearer. Whoever holds one is the agent.
DelegationSHIELD_DELEGATIONoff No user is bound to the action; the agent acts on its own authority.
The failure mode is silence, not error

Every one of these defaults fails open and quiet. Send a perfectly valid Keycloak token to a Shield whose provider list is the default and nothing rejects it. the token is simply never read, and the header is used instead. The request succeeds. The audit record says the role was header-sourced, and that is the only place the difference is visible.

So the deployment check is not "does a valid token work". It is does an invalid one fail. Present a forged role alongside a signed token and confirm the signed claim wins.

Rollout order each step is safe alone; none of them is optional

#StepWhy this order
1Enable the workload provider you actually use (oidc_sa for Keycloak or K8s, spiffe for a mesh) Nothing above L0 can be verified until something at L0 is. This alone changes no decision, so it is safe to ship first.
2Set SHIELD_ROLE_BINDING=prefer Signed claims start winning over headers. prefer falls back to the header when no claim exists, so unmigrated callers keep working.
3Watch role_source in the audit log Every caller still reading header is one that has not migrated. This is the list you need before step 4.
4strict / required Unverified claims are now refused. Safe only once step 3 is empty.

Verify a live deployment with examples/langchain/verify_deployment.py, it reports role provenance and exits non-zero when a layer is not actually enforcing.