Skip to main content

Set up Microsoft Entra agent identity

Self-Hosted Kubernetes

Give each autonomous agent its own Microsoft Entra identity so AIControls can verify who is calling and control which MCP tools it can use. This guide walks you through a two-agent setup: Agent A can read, and Agent B can read and write.

This guide uses Microsoft Entra Agent ID, not a human account, a shared API key, or an ordinary app registration renamed to look like an agent. You create native agent identities from agent identity blueprints. A blueprint holds the credential that lets its agents request tokens.

Your agents and AIControls can run in your existing Kubernetes cluster, including DigitalOcean. They do not need to run in Azure or use a separate identity-server VM. Entra availability and licensing are separate prerequisites; this guide does not imply that Entra is free.

For people signing in to the AIControls console, use Admin console SSO instead. For agents using Kubernetes-issued tokens, use ServiceAccount identity.

Before you start​

  • An Entra tenant with Agent ID available. Ask your tenant administrator to confirm access and licensing.
  • Permission to create an API app registration, agent blueprints, and agent identities, and to upload certificates and assign application roles. Microsoft lists the required blueprint administrator roles; certificate upload requires the appropriate Agent ID administrator access.
  • Access to Microsoft Graph Explorer in that same tenant for the role-assignment steps.
  • AIControls installed with Helm, an admin login, and a test MCP server connected to the gateway.
  • OpenSSL and kubectl on your setup machine, plus an agent client that can use certificate credentials and send a bearer token on MCP requests.
  • HTTPS access from agents to Entra's token endpoint and the gateway, and from AIControls to Entra's discovery and public signing-key endpoints. Keep certificate verification enabled.

Use an isolated test namespace and harmless test records. You do not need to change your existing human SSO configuration.

Keep these IDs separate​

Copy the following values as you complete the steps. They are identifiers, not passwords.

PlaceholderWhere it comes fromUsed for
TENANT_IDEntra tenant overviewToken endpoint and trusted issuer
API_CLIENT_IDProtected API's Application (client) IDToken audience and API scope
API_SP_OBJECT_IDProtected API's Enterprise application Object IDRole-assignment resourceId and URL
BLUEPRINT_A_CLIENT_ID, BLUEPRINT_B_CLIENT_IDEach blueprint's application/client ID (appId)First token request and certificate assertion
AGENT_A_ID, AGENT_B_IDEach native agent identity's idRole-assignment principalId and second token request
READ_ROLE_ID, WRITE_ROLE_IDThe API's appRoles entriesRole-assignment appRoleId

The API's client ID, its application-object ID, and its service-principal object ID are different values. Do not substitute one for another.

Create the Entra identities and permissions​

  1. Register the API that AIControls protects.

    In the Entra admin center, open Entra ID → App registrations → New registration. Name it aicontrols-agent-api and choose accounts in your organization only. This autonomous flow does not need a redirect URI.

    Copy its Application (client) ID as API_CLIENT_ID. Under Expose an API, set the Application ID URI to api://API_CLIENT_ID, replacing the placeholder with that ID.

    In the Microsoft Graph manifest, set api.requestedAccessTokenVersion to 2. Preserve the other manifest fields. The relevant fragment is:

    {
    "api": {
    "requestedAccessTokenVersion": 2
    }
    }

    The API controls the access-token version. Using the /v2.0/token endpoint alone does not ensure a v2 access token.

  2. Create read and write application roles.

    In that API registration, open App roles → Create app role and add:

    Display nameValueAllowed member typesEnabled
    Read MCP recordsMcp.ReadApplicationsYes
    Update MCP recordsMcp.WriteApplicationsYes

    These are application permissions, not delegated user scopes. They do not grant Kubernetes RBAC access. AIControls will enforce their meaning through a policy later in this guide.

    In Graph Explorer, sign in to the same tenant and consent to Application.Read.All for the lookup requests in this guide. Run this GET request after replacing API_CLIENT_ID:

    GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq 'API_CLIENT_ID'&$select=id,displayName,appId,appRoles

    Save the returned service principal's id as API_SP_OBJECT_ID. In appRoles, find the entries whose value is Mcp.Read or Mcp.Write and save their IDs. These role IDs belong to the API; they are not agent IDs.

  3. Create one blueprint and one native identity for each test agent.

    Open Entra ID → Agents → Agent blueprints → New agent blueprint. Create aicontrols-agent-a-blueprint, assign an owner and sponsor, and record its application/client ID. Repeat for aicontrols-agent-b-blueprint.

    The admin-center blueprint wizard creates both the blueprint and its blueprint principal. If you provision through Graph instead, follow Microsoft's full workflow, including the blueprint-principal creation step.

    Next, open Agents → Agent identities → New agent identity. Create aicontrols-agent-a from A's blueprint and aicontrols-agent-b from B's blueprint. Assign owners and sponsors and record each agent identity ID. See Microsoft's agent identity creation guide if your tenant's preview UI differs.

    Verify each identity in Graph Explorer:

    GET https://graph.microsoft.com/v1.0/servicePrincipals/AGENT_A_ID/microsoft.graph.agentIdentity?$select=id,displayName,servicePrincipalType,agentIdentityBlueprintId

    Repeat with AGENT_B_ID. Expect servicePrincipalType: "ServiceIdentity" and the correct agentIdentityBlueprintId. Native agent identities inherit from service principals; creating only ordinary app registrations does not reproduce this setup.

    Separate blueprints and credentials make the two-agent isolation test easier to verify. Entra also supports multiple agents under a blueprint, but anyone holding that blueprint's credential has a broader trust boundary to manage.

  4. Generate separate certificates and upload only the public parts.

    Run these commands in a private working directory outside your source repository:

    umask 077
    mkdir -p entra-agent-credentials
    cd entra-agent-credentials
    for agent in agent-a agent-b; do
    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
    -aes-256-cbc -out "${agent}.key"
    openssl req -new -x509 -sha256 -days 90 \
    -key "${agent}.key" -out "${agent}.crt" \
    -subj "/CN=aicontrols-${agent}"
    done

    OpenSSL asks you to choose a private-key passphrase, then asks for it again when creating the certificate. Keep each passphrase in your secret manager. The .key file is private; the .crt file is the public certificate.

    In Agent blueprints, open A's blueprint, then Developer settings → Credentials → Certificates → Upload certificate. Upload agent-a.crt. Repeat with agent-b.crt on B's blueprint. Microsoft's certificate-management instructions describe this screen.

    Do not upload private keys, put them in Git, or give either agent the other agent's key. These self-signed certificates are client credentials for this test, not gateway TLS certificates. For production, plan certificate rotation or evaluate workload identity federation separately.

  5. Assign the API roles to the agent identities.

    In Graph Explorer, consent to the delegated permissions Application.Read.All and AppRoleAssignment.ReadWrite.All. Your signed-in administrator must also have an active, supported Entra role for assigning application permissions. Consent and the administrator role are separate requirements; an eligible PIM role must be activated. Follow Microsoft's app-role assignment permission requirements.

    These Graph permissions are for the administrator's setup session. Do not grant them to your runtime agents just to let agents call AIControls.

    Use this request to give A read access:

    POST https://graph.microsoft.com/v1.0/servicePrincipals/API_SP_OBJECT_ID/appRoleAssignedTo
    Content-Type: application/json

    {
    "principalId": "AGENT_A_ID",
    "resourceId": "API_SP_OBJECT_ID",
    "appRoleId": "READ_ROLE_ID"
    }

    Replace every placeholder. A successful assignment returns 201 Created. Repeat the request for the remaining rows:

    AgentAssign these roles
    AMcp.Read only
    BMcp.Read and Mcp.Write, using one POST per role

    Check each agent separately:

    GET https://graph.microsoft.com/v1.0/servicePrincipals/AGENT_A_ID/appRoleAssignments
    GET https://graph.microsoft.com/v1.0/servicePrincipals/AGENT_B_ID/appRoleAssignments

    Match resourceId and appRoleId to your API and role IDs. Seeing A's assignment does not prove that B's assignment succeeded. After any assignment change, request a fresh API token; an existing token does not gain new roles.

Configure the agent's token exchange​

The agent requests two tokens. AIControls does not perform this exchange for the agent.

TokenPurposeSend to AIControls?
T1Lets the blueprint act for its associated agent identityNo
T2Lets that agent call your protected APIYes
  1. Create a short-lived certificate assertion for the blueprint.

    Use your OAuth library's certificate support to sign a JWT with the matching blueprint's private key. Follow Microsoft's certificate assertion format: iss and sub are the blueprint client ID, aud is your tenant's token endpoint, jti is unique, and nbf/iat use the current time. Set exp about five minutes later. Microsoft's current guidance uses PS256 and the certificate's SHA-256 thumbprint in x5t#S256.

    The assertion is a credential, not the API access token. Keep it out of logs.

  2. Request T1 using the blueprint credential.

    For A, send the following form-encoded request. Your HTTP client must URL-encode the form values.

    POST https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded

    client_id=BLUEPRINT_A_CLIENT_ID&scope=api://AzureADTokenExchange/.default&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=CERTIFICATE_SIGNED_ASSERTION&fmi_path=AGENT_A_ID

    Keep the response's access_token in memory as T1. The fmi_path must name an agent created from this blueprint.

  3. Exchange T1 for the API token, T2.

    POST https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded

    client_id=AGENT_A_ID&scope=api://API_CLIENT_ID/.default&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=T1_ACCESS_TOKEN

    Use the returned access_token as T2. Repeat both exchanges for B with B's blueprint, certificate, and identity ID. Microsoft's autonomous-agent authentication guide describes the protocol and SDK options.

    Request your API's scope, not https://graph.microsoft.com/.default. A Microsoft Graph token is not an AIControls API token.

  4. Validate T2 before connecting the agent.

    Use a JWT library with Entra's discovery document and public signing keys to verify the signature, issuer, audience, and token lifetime. The discovery URL is:

    https://login.microsoftonline.com/TENANT_ID/v2.0/.well-known/openid-configuration

    After verification, check these claims locally:

    ClaimExpected value
    ver2.0
    isshttps://login.microsoftonline.com/TENANT_ID/v2.0
    audAPI_CLIENT_ID as a bare GUID, not the blueprint ID, api://API_CLIENT_ID, or the api://.../.default scope
    tidYour tenant ID
    oid and azpThe intended native agent ID in this tested autonomous flow
    rolesA: Mcp.Read; B: Mcp.Read and Mcp.Write
    expA future expiry time
    Scope and audience are different

    This guide sets the protected API's api.requestedAccessTokenVersion to 2. Although you request api://API_CLIENT_ID/.default, a v2 access token's aud is the API client GUID. Keep that GUID in the gateway configuration below; do not add the api:// URI as another audience. URI-form audiences can occur in v1 tokens, which also need version-appropriate issuer validation. See Microsoft's audience claim reference.

    Decoding a JWT without checking its signature is not validation. Do not paste live tokens into online decoders or tickets. Optional xms_* claims were absent in the tested tokens; do not use their presence as the only proof of agent identity.

Connect AIControls and the Kubernetes workloads​

  1. Configure the gateway to trust your Entra tenant and API.

    In the console, open Settings → SSO / OIDC → Agent tokens. Under Agent identity providers, select Add Entra tenant, enter a name, your Tenant ID and the protected API's client ID as Audience (API_CLIENT_ID), then select Save & verify. Leave Name claim empty to show each agent's app_displayname.

    An agent identity provider only authenticates Entra app-only agent tokens, never people, and it doesn't change the Agent token issuer above, so the same gateway keeps trusting Kubernetes ServiceAccount tokens. Agents are identified by their Entra application ID.

    note

    Earlier versions of this guide pointed the agent token issuer itself at the Entra tenant. Agents verified that way are identified by their service-principal object ID, so after you switch to an agent identity provider their identity rows, budgets and policies keyed on that object ID don't carry over. Re-key those policies and budgets on the application ID, then reset the Agent token issuer to your Kubernetes issuer (or Use the configuration file value).

    Alternative: a dedicated test gateway that trusts only Entra

    On an isolated gateway with no Kubernetes agents, you can instead set the Agent token issuer to the Entra tenant. In the console, set Issuer to https://login.microsoftonline.com/TENANT_ID/v2.0 and Audience to API_CLIENT_ID. For an unattended install, merge this fragment into the Helm values instead:

    identity:
    clusterOIDCIssuer: "https://login.microsoftonline.com/TENANT_ID/v2.0"
    clusterOIDCAudience: "API_CLIENT_ID"
    requireVerifiedIdentity: true
    jwksCacheTTLSeconds: 300
    agentCacheTTLSeconds: 60

    Despite the clusterOIDC field names, the issuer here is Entra, not the Kubernetes API server. The chart renders validationMode: oidc when clusterOIDCIssuer is set. Do not switch to passthrough to work around a validation error.

    If you use Helm, apply the values with the Helm installation or upgrade workflow. A value saved in Settings → SSO / OIDC → Agent tokens overrides the file; Current value on that tab shows which one applies.

    This changes the agent-token trust configuration for that gateway. Do not replace a shared gateway's Kubernetes issuer without planning for its existing clients. Admin-console SSO is a separate configuration.

    For a test without external policy sources, set policy.sources: [] as described in the installation guide.

  2. Mount each agent's own credential.

    Create the test namespace, then run these commands from the certificate directory:

    kubectl create namespace entra-agent-test
    kubectl -n entra-agent-test create secret generic agent-a-certificate \
    --from-file=certificate.pem=agent-a.crt \
    --from-file=private-key.pem=agent-a.key
    kubectl -n entra-agent-test create secret generic agent-b-certificate \
    --from-file=certificate.pem=agent-b.crt \
    --from-file=private-key.pem=agent-b.key

    Mount the matching Secret read-only in each agent's Deployment. Provide the encrypted key's passphrase through your secret-injection mechanism, and configure your agent's OAuth library to read it. Do not put the passphrase in the image, Helm values, command line, or logs.

    Use a dedicated ServiceAccount per agent and set automountServiceAccountToken: false if the agent does not need Kubernetes API access. The ServiceAccount does not replace the Entra identity: the certificate-based exchange authenticates MCP requests.

    Restrict Secret access with Kubernetes RBAC, use restrictive volume permissions compatible with the container's user/group, and keep the agents from reaching the MCP backend directly. Allow only the gateway and required Entra/DNS traffic. Kubernetes Secrets need appropriate encryption-at-rest and access controls; base64 encoding is not encryption.

  3. Send the final token to the MCP gateway.

    Configure the agent's MCP client to use the gateway's Streamable HTTP endpoint, such as https://YOUR_GATEWAY_HOST/mcp. Send this header on initialization and subsequent requests:

    Authorization: Bearer T2_ACCESS_TOKEN

    Use the gateway's trusted TLS certificate or your organization's CA bundle. Do not disable TLS verification. Refresh the token before it expires, and update the request header when it changes.

    Do not send T1, the certificate assertion, or an admin-console login token. X-User-Token is for a separate delegated-human identity and is not needed here. Do not use agent-name headers as proof of identity.

    For a non-Kubernetes JWT subject, AIControls resolves object.agent.agentId from the verified sub claim. A Kubernetes namespace is not automatically inferred from an Entra token. Use verified token claims for the tenant and native agent checks below.

Enforce read and write permissions​

Entra puts application roles in the token; AIControls needs a policy to map those roles to tools. Creating roles alone does not protect MCP operations.

In Policies, add the following YAML policy and enable enforcement on your isolated test gateway. Replace the three ID placeholders first. This example expects a test backend with read_record and update_record; use your actual tool names if they differ.

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: entra-agent-tool-access
annotations:
proxy.nirmata.io/enforcement-mode: deny
spec:
evaluation:
mode: JSON
matchConditions:
- name: mcp-tool-call
expression: object.mcp.tool != ""
validations:
- expression: >-
object.identity.verified &&
"tid" in object.agent.tokenClaims &&
object.agent.tokenClaims.tid == "TENANT_ID"
message: A verified identity from the approved Entra tenant is required
- expression: >-
"oid" in object.agent.tokenClaims &&
"azp" in object.agent.tokenClaims &&
object.agent.tokenClaims.oid in ["AGENT_A_ID", "AGENT_B_ID"] &&
object.agent.tokenClaims.azp == object.agent.tokenClaims.oid
message: Only the approved native agent identities can call these tools
- expression: >-
"roles" in object.agent.tokenClaims &&
((object.mcp.tool == "read_record" &&
"Mcp.Read" in object.agent.tokenClaims.roles) ||
(object.mcp.tool == "update_record" &&
"Mcp.Write" in object.agent.tokenClaims.roles))
message: The agent does not have permission to use this tool
validationActions: [Deny]

This policy denies other tool names and other agent IDs on that gateway. It does not grant access to a particular record, Kubernetes resource, or tenant's business data. Add those resource-level restrictions separately. Other enabled policies can also deny a request; do not disable unrelated controls to force a test to pass.

See Write a policy for policy deployment and Policy context for the available fields.

Show readable agent names​

By default the console identifies each agent by its Entra application ID. To show the application's display name instead, add the app_displayname optional claim to the access tokens of the protected API: in the API's app registration, open Token configuration, select Add optional claim, choose the Access token type and select app_displayname. Then request a fresh token.

No AIControls configuration is needed: for an Entra issuer, AIControls reads app_displayname from the verified token by default. To read a different claim, set Name Claim in Settings → SSO / OIDC. It applies to agent tokens whose issuer matches the provider configured there, which is the case when that provider is the same Entra tenant. See OIDC provider fields → Readable name. The name is display text only. Policies, budgets and audit records keep using the application ID, so policies should match on the ID, never on the name.

Verify the complete path​

Run these checks with synthetic records. Check the MCP result and whether the backend executed the tool; a policy denial can be returned inside an HTTP 200 JSON-RPC response.

TestExpected result
A calls read_recordAllowed and attributed to A
A calls update_recordDenied; backend does not execute the write
B calls read_record and update_recordBoth allowed and attributed to B
Either agent calls another toolDenied by the example policy
A supplies B's name in a headerStill identified as A; no write access gained
Missing, malformed, tampered, or wrong-audience tokenRejected before backend execution
Expired token, including one used shortly before expiryRejected even when the identity cache is warm
A's blueprint requests a token for B's identityRejected by Entra

Open Activity → Gateway in the AIControls console and select the test time range. Verify each agent's identity, tool, decision, and policy. Compare the UI with persisted audit records or an audit export; successful token validation alone does not prove Kubernetes execution, policy enforcement, or complete auditing.

The console account is separate from both agents. A local dashboard email/password signs in a person; it is not an Entra agent credential.

Troubleshooting​

SymptomCheck
Graph returns 403 Authorization_RequestDeniedConfirm both Graph consent and the signed-in account's active Entra administrator role. Application.Read.All alone cannot assign roles. Check the target agent and resource IDs; do not assume B has the same assignments as A.
GET shows only A's assignmentQuery B's appRoleAssignments explicitly. A failed POST creates no assignment. The assignment object's own id is not the role's appRoleId.
A token has no Mcp.Read or Mcp.WriteAssign application roles to the native agent service principal for this API, then acquire a new T2 token. Check the token audience.
Issuer mismatch or ver: 1.0Set the protected API's api.requestedAccessTokenVersion to 2, request a fresh token, and use the exact tenant-specific v2 issuer.
Audience mismatchUse the API client GUID as the expected v2 audience, not api://API_CLIENT_ID, the blueprint ID, API service-principal object ID, Graph audience, or scope string. If aud is a URI, check ver and the protected API's api.requestedAccessTokenVersion, then acquire a fresh v2 token rather than broadening the accepted audiences.
Token exchange failsCheck the uploaded certificate, private key, certificate dates, machine clock, blueprint client ID, and fmi_path. A must use A's blueprint and certificate.
Valid token, but agent resolution failsCheck whether a platform connection requires a registered-agent mapping. Do not remove that connection to bypass the registration requirement.
The gateway still uses an old issuerCheck saved UI settings and the effective runtime configuration, not only Helm values.
UI shows an empty Kubernetes namespaceAn Entra token does not contain a Kubernetes ServiceAccount subject. Configure a trusted metadata source if you need workload attribution.
Settings cannot be saved through a local viewerA read-only viewer is for inspection. Use the normal authenticated admin endpoint for configuration; do not treat the viewer's 403 as an Entra error.