Set up Microsoft Entra agent identity
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
kubectlon 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.
| Placeholder | Where it comes from | Used for |
|---|---|---|
TENANT_ID | Entra tenant overview | Token endpoint and trusted issuer |
API_CLIENT_ID | Protected API's Application (client) ID | Token audience and API scope |
API_SP_OBJECT_ID | Protected API's Enterprise application Object ID | Role-assignment resourceId and URL |
BLUEPRINT_A_CLIENT_ID, BLUEPRINT_B_CLIENT_ID | Each blueprint's application/client ID (appId) | First token request and certificate assertion |
AGENT_A_ID, AGENT_B_ID | Each native agent identity's id | Role-assignment principalId and second token request |
READ_ROLE_ID, WRITE_ROLE_ID | The API's appRoles entries | Role-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
Register the API that AIControls protects.
In the Entra admin center, open Entra ID → App registrations → New registration. Name it
aicontrols-agent-apiand 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 toapi://API_CLIENT_ID, replacing the placeholder with that ID.In the Microsoft Graph manifest, set
api.requestedAccessTokenVersionto2. Preserve the other manifest fields. The relevant fragment is:{"api": {"requestedAccessTokenVersion": 2}}The API controls the access-token version. Using the
/v2.0/tokenendpoint alone does not ensure a v2 access token.Create read and write application roles.
In that API registration, open App roles → Create app role and add:
Display name Value Allowed member types Enabled Read MCP records Mcp.ReadApplications Yes Update MCP records Mcp.WriteApplications Yes 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.Allfor the lookup requests in this guide. Run this GET request after replacingAPI_CLIENT_ID:GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq 'API_CLIENT_ID'&$select=id,displayName,appId,appRolesSave the returned service principal's
idasAPI_SP_OBJECT_ID. InappRoles, find the entries whosevalueisMcp.ReadorMcp.Writeand save their IDs. These role IDs belong to the API; they are not agent IDs.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 foraicontrols-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-afrom A's blueprint andaicontrols-agent-bfrom 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,agentIdentityBlueprintIdRepeat with
AGENT_B_ID. ExpectservicePrincipalType: "ServiceIdentity"and the correctagentIdentityBlueprintId. 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.
Generate separate certificates and upload only the public parts.
Run these commands in a private working directory outside your source repository:
umask 077mkdir -p entra-agent-credentialscd entra-agent-credentialsfor agent in agent-a agent-b; doopenssl 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}"doneOpenSSL 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
.keyfile is private; the.crtfile is the public certificate.In Agent blueprints, open A's blueprint, then Developer settings → Credentials → Certificates → Upload certificate. Upload
agent-a.crt. Repeat withagent-b.crton 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.
Assign the API roles to the agent identities.
In Graph Explorer, consent to the delegated permissions
Application.Read.AllandAppRoleAssignment.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/appRoleAssignedToContent-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:
Agent Assign these roles A Mcp.ReadonlyB Mcp.ReadandMcp.Write, using one POST per roleCheck each agent separately:
GET https://graph.microsoft.com/v1.0/servicePrincipals/AGENT_A_ID/appRoleAssignmentsGET https://graph.microsoft.com/v1.0/servicePrincipals/AGENT_B_ID/appRoleAssignmentsMatch
resourceIdandappRoleIdto 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.
| Token | Purpose | Send to AIControls? |
|---|---|---|
| T1 | Lets the blueprint act for its associated agent identity | No |
| T2 | Lets that agent call your protected API | Yes |
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:
issandsubare the blueprint client ID,audis your tenant's token endpoint,jtiis unique, andnbf/iatuse the current time. Setexpabout five minutes later. Microsoft's current guidance usesPS256and the certificate's SHA-256 thumbprint inx5t#S256.The assertion is a credential, not the API access token. Keep it out of logs.
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/tokenContent-Type: application/x-www-form-urlencodedclient_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_IDKeep the response's
access_tokenin memory as T1. Thefmi_pathmust name an agent created from this blueprint.Exchange T1 for the API token, T2.
POST https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/tokenContent-Type: application/x-www-form-urlencodedclient_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_TOKENUse the returned
access_tokenas 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.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-configurationAfter verification, check these claims locally:
Claim Expected value ver2.0isshttps://login.microsoftonline.com/TENANT_ID/v2.0audAPI_CLIENT_IDas a bare GUID, not the blueprint ID,api://API_CLIENT_ID, or theapi://.../.defaultscopetidYour tenant ID oidandazpThe intended native agent ID in this tested autonomous flow rolesA: Mcp.Read; B:Mcp.ReadandMcp.WriteexpA future expiry time Scope and audience are differentThis guide sets the protected API's
api.requestedAccessTokenVersionto2. Although you requestapi://API_CLIENT_ID/.default, a v2 access token'saudis the API client GUID. Keep that GUID in the gateway configuration below; do not add theapi://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
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'sapp_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.
noteEarlier 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.0and Audience toAPI_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: truejwksCacheTTLSeconds: 300agentCacheTTLSeconds: 60Despite the
clusterOIDCfield names, the issuer here is Entra, not the Kubernetes API server. The chart rendersvalidationMode: oidcwhenclusterOIDCIssueris set. Do not switch topassthroughto 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.Mount each agent's own credential.
Create the test namespace, then run these commands from the certificate directory:
kubectl create namespace entra-agent-testkubectl -n entra-agent-test create secret generic agent-a-certificate \--from-file=certificate.pem=agent-a.crt \--from-file=private-key.pem=agent-a.keykubectl -n entra-agent-test create secret generic agent-b-certificate \--from-file=certificate.pem=agent-b.crt \--from-file=private-key.pem=agent-b.keyMount 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: falseif 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.
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_TOKENUse 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-Tokenis 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.agentIdfrom the verifiedsubclaim. 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.
| Test | Expected result |
|---|---|
A calls read_record | Allowed and attributed to A |
A calls update_record | Denied; backend does not execute the write |
B calls read_record and update_record | Both allowed and attributed to B |
| Either agent calls another tool | Denied by the example policy |
| A supplies B's name in a header | Still identified as A; no write access gained |
| Missing, malformed, tampered, or wrong-audience token | Rejected before backend execution |
| Expired token, including one used shortly before expiry | Rejected even when the identity cache is warm |
| A's blueprint requests a token for B's identity | Rejected 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
| Symptom | Check |
|---|---|
Graph returns 403 Authorization_RequestDenied | Confirm 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 assignment | Query 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.Write | Assign 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.0 | Set the protected API's api.requestedAccessTokenVersion to 2, request a fresh token, and use the exact tenant-specific v2 issuer. |
| Audience mismatch | Use 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 fails | Check 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 fails | Check 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 issuer | Check saved UI settings and the effective runtime configuration, not only Helm values. |
| UI shows an empty Kubernetes namespace | An 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 viewer | A 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. |