Skip to content

All posts

AI

How to map AI agents and humans to one identity model with OIDC

Ritesh Patel

AI: How to map AI agents and humans to one identity model with OIDC

Map AI agents and humans to one identity model by putting both behind one OIDC issuer, registering each agent as its own client, and mapping the same claims to the username your cluster or API records. Kubernetes accepts tokens from any OIDC provider that meets its requirements, so the mapping lives in the authenticator configuration rather than in each agent.

This guide uses Microsoft Entra ID as the worked example and Kubernetes as the consumer. It covers agents that act for a signed-in user through a delegated session. An unattended, app-only agent gets a different token and needs its own claim mapping, which this guide does not cover. The field names and defaults come from the sources listed at the end, and the design choices are our recommendation.

Why one issuer covers people and agents

OpenID Connect is an identity layer on OAuth 2.0. It gives a relying party an ID token that says who signed in, and the relying party checks that token against the issuer’s public keys. The Microsoft identity platform describes the same flow: the app sends the user to the authorize endpoint, gets an ID token back, and validates it.

The practical point is that the issuer does not care whether the principal is a person or a process. It signs whatever principal it registered. So one issuer can serve both, and your authorization layer only needs to read one token format. The split happens in the claims, which is where the next section starts.

Read the claims that identify each principal

Pick the claim your system keys on before you write any rule. The Entra claims reference states which values are stable and which are not. sub is immutable and pairwise, so each application ID gets its own value, and two apps see two values for one user. oid is the immutable object ID of the user account, and it matches across apps in the same tenant. preferred_username can change over time, so it suits display, not authorization. For groups, JWT tokens carry up to 200 object IDs, and beyond that the groups claim is left out.

Claim What the Entra reference says Use it for
sub Immutable. A pairwise identifier, unique to the application ID, so two apps see two values for one user A per-application key for one principal
oid Immutable object ID for the user account. Same value across apps in the same tenant Correlating one user across your own apps
tid The tenant GUID the user signed in to Restricting which tenants may sign in
preferred_username Mutable and can change over time Display and hints only, never authorization
groups Limited to 200 object IDs in JWT tokens. Above that, the claim is left out and hasgroups is set Group rules, only after you handle the overage case

The reference advises using sub or oid alone to store per-user data, because oid is a GUID and sub is a pairwise identifier unique to the application. It also warns that oid and sub change across tenants by design, so do not use them to follow one user between tenants. Read the table as a reference for what each claim can and cannot carry.

Step 1: Register each agent as its own client

  1. Create one app registration per agent, or per agent deployment that needs a separate permission set.
  2. Enable ID tokens on the app. Entra does not issue them by default. If you request one from an app without it, the authorize endpoint returns an unsupported_response error.
  3. Record the application (client) ID. For ID tokens, Entra sets aud to that ID, and the Kubernetes audience must match it.
  4. Because sub is pairwise per application ID, one user signing in through two agent clients gets two sub values. A rule keyed on sub then applies to one agent client for that user only. Check the token your provider issues before you write rules against it.

Step 2: Configure the API server to trust the issuer

The Kubernetes docs list the command-line flags for the JWT authenticator. Four of them decide how a token becomes a username and a group set.

Flag Default What it controls
--oidc-issuer-url Required The issuer. The URL scheme must be https
--oidc-client-id Required The audience all tokens must be issued for
--oidc-username-claim sub The claim used as the username. Non-email claims get the issuer URL as a prefix by default
--oidc-groups-claim None (unset) The claim to read groups from. Unset means the token’s groups are not imported, so a RoleBinding that names a group grants nothing. The claim must be an array of strings

The default username prefix matters. If you leave --oidc-username-claim at sub and set no prefix, the username carries the issuer URL. Set the prefix on purpose, and keep it short enough to read in an audit log.

Flags cannot express everything you need, such as an audience list or a tenant check. The structured file can. Its keys are documented in the Kubernetes authentication page, and this example uses only those keys.

apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer:
    url: https://login.microsoftonline.com/<tenant-id>/v2.0
    audiences:
    - <app-client-id>
    audienceMatchPolicy: MatchAny
  claimValidationRules:
  - expression: 'claims.tid == "<tenant-id>"'
    message: the tid claim must match the tenant
  claimMappings:
    username:
      claim: "sub"
      prefix: "entra:"
    groups:
      claim: "groups"
      prefix: "entra:"

The groups mapping is what makes group-based RoleBindings work. Leave it out and the token’s groups are ignored. Entra omits the claim above 200 groups, as covered below. Two rules from the same page apply here. If you pass --authentication-config together with any --oidc-* flag, the API server reports a misconfiguration and exits. And the issuer value in the discovery document must match issuer.url, so check that the discovery response of your tenant returns the same string before you deploy.

Step 3: Carry the delegating person separately

An agent usually acts for a person, and the audit trail needs both. Keep the subject as the agent’s identity and record the person as a second fact. The API server’s username.expression accepts a CEL expression that returns a string, so you can build a name from more than one claim. The docs show a CEL example that appends a fixed string to a claim.

Whether the token carries the delegating user depends on your provider. The Entra pages we fetched do not define a claim for an agent’s delegator, so confirm with your provider which claim, if any, names the person before you write the expression.

Gotchas from the fine print

  • Kubernetes says a self-hosted provider must use a certificate signed by a CA with the CA flag set to TRUE, even when that CA is self-signed. Go’s TLS client is strict about this.
  • Entra says to validate ID token signatures and claims with a token validation library, not by hand.
  • The JWT authenticator also validates the service account tokens Kubernetes issues. The structured config’s issuer URL must not conflict with --service-account-issuer.
  • Kubernetes says distributed claims do not work through CEL expressions. Plan group mapping around that.
  • The Kubernetes limits list also requires the provider to run over TLS with non-obsolete ciphers.

Decision checklist

  1. One issuer for people and agents, unless a trust boundary or compliance rule says otherwise.
  2. One app registration per agent, so each has its own audience and permissions. In a delegated session sub identifies the person for that application, so record the agent client separately.
  3. Username from sub or oid, never from preferred_username or a mutable display name.
  4. Audience pinned to the client ID, with audienceMatchPolicy set to MatchAny when you list several.
  5. Tenant restricted by a claim check, so a token from another tenant is refused.
  6. The delegating person recorded in the audit trail, from a claim your provider actually issues.

Where SPIFFE IDs fit

Some workloads already carry a SPIFFE ID. A SPIFFE ID is a URI with the spiffe scheme, a trust domain and a path, for example spiffe://trust-domain-name/path. The standard requires a lowercase trust domain that uses only letters, numbers, dots, dashes and underscores, and a path with no trailing slash and no percent-encoded characters. Implementations must support URIs up to 2048 bytes and should not generate longer ones.

Trust domain names are not globally unique, and two trust domains that pick the same name cannot federate. Use SPIFFE IDs to name the workload, and use OIDC tokens for people and for the sessions that agents hold on their behalf. Keep the two namespaces separate in your policy so one cannot be read as the other.

Next steps

Test the mapping with one token from each identity type: a person, an agent client, and a service account. For each token, record the username the API server shows and confirm that renaming the user’s display name leaves it unchanged. Then check one user above the groups overage limit. The rest of this series covers what agent identity adds beyond a service account, and how to restrict which MCP servers an agent can use. For the wider agent governance context, start at the governing AI agents hub, and see the AIControls platform page for the product.

Sources

Frequently asked questions

Can an AI agent and a person use the same OIDC issuer?

Yes. An OIDC issuer signs tokens for any principal it registers, so one issuer can serve people and agents. Give each agent its own client registration, so you can scope its permissions and audience separately. In a delegated session the token's `sub` still identifies the signed-in person, per application, not the agent. Keep the agent client in a separate claim, if your provider supplies one, so audit logs can show both. An unattended app-only agent uses a different token flow that this guide does not cover.

Why not use the email claim as the Kubernetes username?

Email addresses can change, and they can be reused. The Kubernetes docs describe sub as the claim expected to be a unique identifier of the end user, and that is the default username claim. Map sub as the username and keep email for display and audit. If you do use email, the API server applies a verified-email check automatically.

Does Kubernetes issue OIDC tokens for my agents?

No. Kubernetes does not provide an OpenID Connect identity provider. It accepts tokens from an existing provider, or one you run, that supports discovery and TLS. Kubernetes issues its own service account tokens, which go through the same JWT authentication path, so keep the issuer you configure for people and agents distinct from the service account issuer.

What breaks when an agent is in more than 200 groups?

For Entra JWT tokens, the groups claim is left out once a user passes the overage limit of 200 object IDs, and the token instead signals that the client should query Microsoft Graph. A group-based rule that only reads the token will then see no groups. Test with a user above the limit before relying on group claims for agent access.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.