Skip to content

Agent Workload Identity

An agent’s name is a selector; its identity is a token an identity provider signed. When an agent profile names an identity provider, every data-plane call from that agent must additionally present X-Agent-Token, verified against that exact provider’s own key set. Without a provider the agent is trusted on the strength of its Virtual Key alone, which is appropriate for local development and not much else.

Configure providers under Agentic → Identities → Identity providers, then bind one to an agent from Assets → Agents → Identity & credentials.

TypeUse it for
entra_agent_idMicrosoft Entra Agent ID. Constrained to approved Microsoft authority hosts.
generic_oidcAny OIDC issuer with a public HTTPS discovery endpoint.
zeroidZeroID-issued agent identities.
spiffeJWT-SVIDs from a SPIFFE issuer, typically an in-cluster SPIRE OIDC discovery provider.

Endpoints are validated before the gateway dials them: no cloud metadata address is ever reachable, and the hostname is resolved by the same client that connects, so a DNS answer cannot change between validation and the connection.

Admit a class of identities from one provider

Section titled “Admit a class of identities from one provider”

Binding a provider to exactly one client id means one provider row per agent — the same issuer, authority, and audience re-entered for every agent in the estate. agent_identity_match_mode generalises that from “this one agent” to “the agents this issuer vouches for”:

ModeAdmits
exact (default)Only the configured agent identity client id.
listAny value in agent_identity_match.
prefixAny value carrying one of the listed prefixes.
anyAny identity the issuer signs for. Correct only when the issuer is dedicated to this workspace’s agents.

Per-agent binding is not weakened by this. Whatever the predicate admits, the agent subject is still pinned to the exact identity the token carries — the predicate decides which identities a provider may speak for, never which agent a given token is. An unrecognised mode collapses to exact, so a typo can never widen a provider.

exp alone leaves the issuer deciding how long a stolen credential stays useful: an issuer minting 24-hour agent tokens has every one of them honoured for its full life, so a token lifted from a log or a crash dump is valid for the rest of the day.

accepted_token_max_ttl_sec on the identity provider is the relying party’s own tolerance:

ValueEffect
0 (default)A bounded default of one hour. Accommodates the common one-hour token while removing the unbounded case.
n > 0Refuse a token from this provider whose total lifetime exceeds n seconds.
n < 0Disable the ceiling. An operator has to choose this deliberately.

This is separate from token_max_ttl_sec, which is mint-side and only shortens the expiry on tokens the gateway itself hands out.

A workload token carrying a jti is refused if it is presented twice. Entries are remembered only until the token would expire anyway, since a token past its exp is already refused by claim validation. Tokens without a jti are not tracked — the claim is optional in JWT and requiring it would break issuers that omit it.

Replay protection stops a credential recovered from a log, a proxy trace, or a crash dump and reused afterwards. It cannot help against an attacker who intercepts a token before its first use, because nothing yet ties the token to the sender. That is what proof-of-possession adds.

Set require_proof_of_possession on an identity provider to refuse a plain bearer token from it. The client then holds a private key, the issuer binds that key’s thumbprint into the token as cnf.jkt, and every request carries a fresh proof signed by the key over the request method and URI (RFC 9449). A stolen token without the key is useless.

  • It is off by default, and should stay off until the issuer actually binds tokens. A token with no cnf.jkt has nothing to prove possession of, so enabling this first denies everything.
  • A token that carries no cnf.jkt behaves exactly as before, so enabling binding at the issuer does not break existing callers ahead of the switch.
  • The Python SDK generates an ephemeral per-process key and needs the optional dependency: pip install "deepintshield[dpop]==2.8.3". See Agents and Agentic governance.

Once an agent’s registration is approved with an identity provider, clearing that provider is an attestation downgrade: the agent would keep every grant it earned while proving less about who it is. PUT /agents/{id}/governance therefore refuses to empty identity_provider_id on an approved agent — changing it requires an explicit re-review.

A workload often announces itself by a friendly name (agent:gisec-demo-agent) while the identity a provider can prove for it is a client id (agent:79330596-…). An expected identity declares, per friendly agent key and workspace, which provider-issued identity that name is expected to prove. It is modelled on a SPIRE registration entry: it names the identity a workload should attest to, and it never attests anything itself.

What a declaration does:

  • Resolves the name before validation and verification. At first sighting, a token that verifies against the declared provider and derives to exactly the expected subject attests the registration under the friendly name. At runtime, a bound agent’s token is verified as the expected identity while the agent keeps its friendly subject - so every allowed_caller tuple, resource grant and delegation stays keyed to agent:<key> and survives the upgrade from VK-trusted to provider-attested.
  • Unlocks an explicit VK-trusted approval. With an expected identity declared, the review form offers Approve as VK-trusted: the agent is approved with no provider, recorded as unattested, and carries its own exit - the declaration says what proof will complete it.
  • Proposes, never promotes. When an approved VK-trusted agent later presents a verified token for exactly its expected identity, the gateway records an attestation upgrade proposal. The call proceeds VK-trusted and nothing on the agent changes until an operator confirms in the review dialog. Confirmation binds the provider, records which provider attested the agent and when, and marks the declaration bound - in one transaction, under row locks, re-checking that the proposal still matches the declaration and that the provider is enabled and tested.

Invariants the layer enforces:

  • A declaration can be satisfied only by a verified token for exactly the expected subject from exactly the expected provider. Selecting the expected provider in a review for a registration that never presented such a token is refused - declaring is not vouching.
  • One name, one identity; one identity, one name per workspace, enforced by the database. Two names can never race for the same credential.
  • A bound declaration is immutable: re-pointing a name at another identity is an identity change and goes through registration review again. Deleting it is refused for the same reason.
  • Zero cost on the hot path: the resolved identity is denormalised onto the agent profile the request already loads, so the runtime never consults the declaration table. A provider-less call that carries no token does no resolution work at all.

Manage declarations from the registration review dialog (Expected identity) or the API:

GET /api/agentic-new/registry/identity-aliases
PUT /api/agentic-new/registry/identity-aliases {agent_key, expected_provider_id, expected_subject}
DELETE /api/agentic-new/registry/identity-aliases/{id}
POST /api/agentic-new/registry/registrations/{id}/attest confirm a verified proposal
POST /api/agentic-new/registry/registrations/{id}/attest/dismiss