Many organizations run agents on AWS and govern identity, security and compliance in Microsoft. The usual assumption is that the two only meet through a client secret copied from one cloud into the other.

They don’t have to. This article shows the calls that let a LangChain agent running on Amazon ECS, using an Amazon Bedrock model, be governed like a Microsoft-hosted agent:

  • it has its own identity in Microsoft Entra, with a named owner and sponsor;
  • it accepts calls only from people Entra has been told may use it;
  • it proves who it is to Entra with a token signed by AWS, so no secret exists anywhere;
  • it sends every request, model call and tool call to Agent 365, which makes it visible in Microsoft Purview, Microsoft Defender and the Microsoft 365 admin center.

The focus is the API and SDK calls: which ones, in what order, and what goes wrong if the order changes. Hosting details (VPC design, load balancer, CI/CD, Terraform layout) are left out on purpose.

Status of the APIs. Entra Agent ID and the Agent Registration API were in preview when this was written (October 2026). Names, permissions and endpoints may change; check the current Microsoft and AWS documentation before relying on them.

Conventions

Values in angle brackets are placeholders: <tenant-id>, <aws-account-id>, <blueprint-app-id>, <agent-identity-id> and so on. Nothing in this article is specific to one environment.

Three identifiers below are constants published by Microsoft, the same in every tenant:

ConstantValue
Azure CLI client application04b07795-8ddb-461a-bbee-02f9e1bf7b46
Agent 365 observability API9b975845-388f-4429-889e-eab1ef63949c
“Default access” app role00000000-0000-0000-0000-000000000000

The idea in one paragraph

AWS can issue a signed token (a JWT) that says “this is IAM role X in account Y”. Entra can be told to trust exactly that issuer and that role, through a federated identity credential. The agent’s task asks AWS for such a token, the Microsoft Entra auth sidecar presents it to Entra, and Entra returns a token for the agent’s own Entra identity. AWS vouches for the workload; Entra decides what the agent is and what it may do.

The moving parts

PartWhat it is
AWS IAM outbound identity federationAn account-level feature. Once enabled, IAM principals can call sts:GetWebIdentityToken and receive a JWT signed by the account’s own issuer.
ECS task roleThe IAM role the containers run as. Its ARN is the subject of the AWS-signed token.
Token helperA small container (about 40 lines) that keeps a fresh AWS-signed token in a file on a volume shared with the sidecar.
Entra auth sidecarThe Microsoft Entra SDK for Agent ID, shipped as a container (mcr.microsoft.com/entra-sdk/auth-sidecar). It does all Entra token work over localhost.
Agent identity blueprintAn Entra application object of type agentIdentityBlueprint. It holds the federated credential and is the parent of the agent identity.
Blueprint principalThe blueprint’s service principal in the tenant. Access assignments are made against it.
Agent identityA service principal of type agentIdentity. This is “who the agent is” in Entra and in Agent 365.
Agent 365 observability SDKThe microsoft-opentelemetry Python package. It instruments LangChain and exports to Agent 365.

One ECS task runs three containers that share localhost: the agent, the sidecar and the token helper.

The whole flow in one picture

Request flow: LangChain agent on Amazon ECS with Microsoft Entra. The numbered steps are described in the sections below.

The calls fall into five phases. Each depends on the one before it.

PhaseWhenWho runs itAPI or SDK
0. Prepare AWSOnce per accountAWS administratorAWS IAM
1. RegisterOnce per agentDeployment pipelineMicrosoft Graph
2. GovernOnce per agentEntra administratorMicrosoft Graph
3. SecureEvery requestThe agent’s taskAWS STS, Microsoft Entra SDK for Agent ID (sidecar)
4. ObserveEvery requestThe agentAgent 365 observability SDK

Prerequisites

  • An AWS account with Amazon Bedrock model access for the model you choose (some model providers require a one-time use-case form per account).
  • A Microsoft tenant with Entra Agent ID and Agent 365 available, and the licensing they require.
  • A user who will be the agent’s sponsor.
  • An identity for the deployment pipeline with these Microsoft Graph application permissions (admin consent required):
AgentIdentityBlueprint.Create
AgentIdentityBlueprint.Read.All
AgentIdentityBlueprint.AddRemoveCreds.All
AgentIdentityBlueprint.UpdateAuthProperties.All
AgentIdentityBlueprint.DeleteRestore.All
AgentIdentityBlueprintPrincipal.Create
AgentIdentityBlueprintPrincipal.Read.All
AgentIdentityBlueprintPrincipal.DeleteRestore.All
AgentIdentity.Create.All
AgentIdentity.Read.All
AgentIdentity.DeleteRestore.All
AgentRegistration.ReadWrite.All

The Phase 2 steps need more: AppRoleAssignment.ReadWrite.All and AgentIdentityBlueprintPrincipal.ReadWrite.All. Keep those with an administrator instead of the pipeline.

The Graph examples use az rest, which signs requests with the signed-in identity. Any HTTP client with a Graph token works the same way. No Azure subscription is needed to host anything; only the Entra tenant is involved.


Phase 0: Prepare AWS (once per account)

0.1 Enable outbound identity federation and note the issuer

aws iam enable-outbound-web-identity-federation
aws iam get-outbound-web-identity-federation-info

Why: this gives the account its own token issuer, a URL of the form https://<issuer-id>.tokens.sts.global.api.aws. Entra will be told to trust that URL in step 1.4.

Order: first. The issuer URL does not exist until the feature is enabled, and step 1.4 needs it.

0.2 Create the task role with exactly two permissions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InvokeModel",
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": [
        "arn:aws:bedrock:<region>:<aws-account-id>:inference-profile/<inference-profile-id>",
        "arn:aws:bedrock:*::foundation-model/<foundation-model-id>"
      ]
    },
    {
      "Sid": "EntraFederationAssertion",
      "Effect": "Allow",
      "Action": "sts:GetWebIdentityToken",
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringEquals": { "sts:IdentityTokenAudience": ["api://AzureADTokenExchange"] }
      }
    }
  ]
}

The role’s trust policy allows ecs-tasks.amazonaws.com to assume it.

Why:

  • sts:GetWebIdentityToken lets the task ask AWS for a signed token about itself. The condition limits it to the single audience Entra uses for token exchange, so the role cannot mint tokens for any other relying party.
  • The Bedrock statement names one model. The role can do nothing else.

Note the role’s ARN. It becomes the subject that Entra trusts. Keep the role name stable: if the ARN changes, the trust in step 1.4 must change with it.


Phase 1: Register the agent (Microsoft Graph)

Six calls. The order is fixed because each one needs an id produced by an earlier one.

1.1 Create the agent identity blueprint

az rest --method POST --url "https://graph.microsoft.com/v1.0/applications" \
  --headers "Content-Type=application/json" --body '{
    "@odata.type": "#microsoft.graph.agentIdentityBlueprint",
    "displayName": "<agent-name>-blueprint",
    "sponsors@odata.bind": ["https://graph.microsoft.com/v1.0/users/<sponsor-object-id>"]
  }'

Why: everything else hangs off the blueprint. It will hold the trust in AWS, expose the API that callers request tokens for, and be the parent of the agent identity.

Details that matter: the @odata.type is what makes this a blueprint and not an ordinary app registration. Save id (object id) and appId from the response.

1.2 Expose an API on the blueprint

az rest --method PATCH --url "https://graph.microsoft.com/v1.0/applications/<blueprint-object-id>" \
  --headers "Content-Type=application/json" --body '{
    "identifierUris": ["api://<blueprint-app-id>"],
    "api": { "oauth2PermissionScopes": [{
      "id": "<new-guid>", "value": "access_agent", "type": "User", "isEnabled": true,
      "adminConsentDisplayName": "Access <agent-name>",
      "adminConsentDescription": "Allow the application to call <agent-name> on behalf of the signed-in user.",
      "userConsentDisplayName": "Access <agent-name>",
      "userConsentDescription": "Allow the application to call <agent-name> on your behalf."
    }]}
  }'

Then pre-authorize the client applications users sign in with (here the Azure CLI):

az rest --method PATCH --url "https://graph.microsoft.com/v1.0/applications/<blueprint-object-id>" \
  --headers "Content-Type=application/json" --body '{
    "api": {
      "oauth2PermissionScopes": [ { "...": "the same scope object as above" } ],
      "preAuthorizedApplications": [{
        "appId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
        "delegatedPermissionIds": ["<the-scope-guid>"]
      }]
    }
  }'

Why two calls: a scope has to exist before it can be pre-authorized, and the api block is replaced as a whole on every PATCH, so the second call repeats the scope.

1.3 Create the blueprint principal

az rest --method POST --url "https://graph.microsoft.com/v1.0/servicePrincipals" \
  --headers "Content-Type=application/json" --body '{
    "@odata.type": "#microsoft.graph.agentIdentityBlueprintPrincipal",
    "appId": "<blueprint-app-id>"
  }'

Why: access assignments (Phase 2) and the agent identity (1.5) both need it. Entra is eventually consistent, so retry on Request_ResourceNotFound or NoBackingApplicationObject.

1.4 Tell Entra to trust the AWS role (the key call)

az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/applications/<blueprint-object-id>/federatedIdentityCredentials" \
  --headers "Content-Type=application/json" --body '{
    "name": "aws-ecs-task-role",
    "description": "AWS IAM outbound identity federation: ECS task role",
    "issuer": "https://<issuer-id>.tokens.sts.global.api.aws",
    "subject": "arn:aws:iam::<aws-account-id>:role/<task-role-name>",
    "audiences": ["api://AzureADTokenExchange"]
  }'

Why: this one object replaces a client secret. It says: “a token signed by this AWS account, about this IAM role, for this audience, may act as this blueprint”. Nothing is stored on either side that could be copied or leaked.

Details that matter:

  • issuer comes from step 0.1, subject is the role ARN from step 0.2.
  • All three values must match the AWS token exactly, including case.
  • Entra accepts only RS256-signed assertions with a single audience. Step 3.1 requests the token accordingly.

1.5 Create the agent identity

az rest --method POST --url "https://graph.microsoft.com/v1.0/servicePrincipals" \
  --headers "Content-Type=application/json" --body '{
    "@odata.type": "#microsoft.graph.agentIdentity",
    "displayName": "<agent-name>",
    "agentIdentityBlueprintId": "<blueprint-app-id>",
    "sponsors@odata.bind": ["https://graph.microsoft.com/v1.0/users/<sponsor-object-id>"]
  }'

Why: this is the identity the agent acts as towards Microsoft services. Save the returned id as <agent-identity-id>.

Order: after 1.3; it fails until the blueprint principal exists.

1.6 Register the agent in the Agent 365 registry

az rest --method POST --url "https://graph.microsoft.com/beta/copilot/agentRegistrations" \
  --headers "Content-Type=application/json" --body '{
    "displayName": "<agent-name>",
    "description": "LangChain chat agent on Amazon ECS with Amazon Bedrock",
    "createdBy": "<sponsor-object-id>",
    "ownerIds": ["<sponsor-object-id>"],
    "sourceAgentId": "<agent-name>",
    "originatingStore": "<how-it-was-created>",
    "sourceCreatedDateTime": "<utc-timestamp>",
    "sourceLastModifiedDateTime": "<utc-timestamp>",
    "agentIdentityBlueprintId": "<blueprint-app-id>",
    "agentIdentityId": "<agent-identity-id>",
    "agentCard": {
      "name": "<agent-name>", "version": "1.0.0",
      "description": "General-purpose chat assistant built with LangChain, hosted on AWS.",
      "capabilities": { "streaming": false, "pushNotifications": false },
      "defaultInputModes": ["text"], "defaultOutputModes": ["text"],
      "skills": [{ "id": "chat", "name": "Chat", "description": "Answers general questions." }]
    }
  }'

Why: this lists the agent in the inventory administrators see (Microsoft 365 admin center > Agents) and ties the entry to the Entra identity. That link is what makes it a managed agent and not just a listed one.


Phase 2: Govern the agent (Entra administrator)

2.1 Add an owner to each object

BODY='{"@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/<owner-object-id>"}'
az rest --method POST --headers "Content-Type=application/json" --body "$BODY" \
  --url 'https://graph.microsoft.com/v1.0/applications/<blueprint-object-id>/owners/$ref'
az rest --method POST --headers "Content-Type=application/json" --body "$BODY" \
  --url 'https://graph.microsoft.com/v1.0/servicePrincipals/<blueprint-principal-id>/owners/$ref'
az rest --method POST --headers "Content-Type=application/json" --body "$BODY" \
  --url 'https://graph.microsoft.com/v1.0/servicePrincipals/<agent-identity-id>/owners/$ref'

Why: the sponsor is the business owner; the owner is the person who technically manages the objects.

2.2 Decide who may call the agent

az ad group create --display-name "<agent-users-group>" --mail-nickname "<agent-users-group>"

az rest --method POST --headers "Content-Type=application/json" \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<blueprint-principal-id>/appRoleAssignedTo" \
  --body '{
    "principalId": "<group-object-id>",
    "resourceId": "<blueprint-principal-id>",
    "appRoleId": "00000000-0000-0000-0000-000000000000"
  }'

Repeat for any non-human caller, such as the pipeline identity that runs smoke tests. If you already have a group for another agent’s users, reusing it gives one place to govern who can use agents wherever they run.

2.3 Turn on “assignment required”

az rest --method PATCH --headers "Content-Type=application/json" \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<blueprint-principal-id>/microsoft.graph.agentIdentityBlueprintPrincipal" \
  --body '{ "appRoleAssignmentRequired": true }'

Why: Entra now refuses to issue a token for the agent to anyone who is not assigned (error AADSTS50105). The decision is made by the identity provider before a request ever reaches AWS.

Order: after 2.2, or you lock out your own callers.

2.4 Let the agent identity send telemetry to Agent 365

az ad sp show --id 9b975845-388f-4429-889e-eab1ef63949c \
  --query "{resourceId:id, role:appRoles[?value=='Agent365.Observability.OtelWrite'].id | [0]}"

az rest --method POST --headers "Content-Type=application/json" \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<observability-sp-id>/appRoleAssignedTo" \
  --body '{
    "principalId": "<agent-identity-id>",
    "resourceId": "<observability-sp-id>",
    "appRoleId": "<otelwrite-role-id>"
  }'

Why: Agent 365 accepts telemetry only from an identity holding Agent365.Observability.OtelWrite.

Order: before the task first starts. The sidecar caches tokens; one issued before the grant has no role until it is refreshed or the task restarts.

Create an Entra ID Governance access review on the users group (POST /identityGovernance/accessReviews/definitions) with a quarterly recurrence, the group owners as reviewers, and a default decision of Deny that is applied automatically. Anyone not confirmed loses access.

A note on the model

On AWS the model call is authorized by AWS IAM (the task role from step 0.2), not by the Entra agent identity. There is no Entra permission to grant for Bedrock. The Entra identity governs who may call the agent and attributes everything the agent does in Agent 365; IAM governs what the task may touch inside AWS.


Phase 3: Secure every request (AWS STS and the Entra auth sidecar)

3.1 Token helper: get the AWS-signed token

import os, time, boto3

sts = boto3.client("sts", region_name=os.environ["AWS_REGION"])
TOKEN_FILE = "/var/run/entra/aws-token"

def refresh() -> None:
    token = sts.get_web_identity_token(
        Audience=["api://AzureADTokenExchange"],
        SigningAlgorithm="RS256",          # the only algorithm Entra accepts here
        DurationSeconds=3600,
    )["WebIdentityToken"]
    tmp = TOKEN_FILE + ".tmp"
    with open(tmp, "w", encoding="ascii") as f:
        f.write(token)
    os.chmod(tmp, 0o644)                   # the sidecar runs as a different user
    os.replace(tmp, TOKEN_FILE)            # atomic: never a half-written file

while True:
    refresh()
    time.sleep(600)                        # refresh well before the token expires

Why: this is the AWS half of the trust created in step 1.4. The token’s issuer is the account’s issuer, its subject is the task role ARN, and its audience is the Entra token-exchange audience.

Details that matter:

  • Write atomically and make the file readable by the sidecar’s user.
  • Log the token’s non-secret claims (iss, sub, aud) once per refresh. When federation fails, comparing these with the federated credential finds the mismatch in seconds.

3.2 Configure the sidecar to use that file

ASPNETCORE_URLS                                              = http://127.0.0.1:5000
AllowedHosts                                                 = localhost;127.0.0.1
AzureAd__Instance                                            = https://login.microsoftonline.com/
AzureAd__TenantId                                            = <tenant-id>
AzureAd__ClientId                                            = <blueprint-app-id>
AzureAd__ClientCredentials__0__SourceType                    = SignedAssertionFilePath
AzureAd__ClientCredentials__0__SignedAssertionFileDiskPath   = /var/run/entra/aws-token
AzureAd__AllowWebApiToBeAuthorizedByACL                      = true

DownstreamApis__a365obs__BaseUrl                             = https://agent365.svc.cloud.microsoft/
DownstreamApis__a365obs__Scopes__0                           = api://9b975845-388f-4429-889e-eab1ef63949c/.default
DownstreamApis__a365obs__RequestAppToken                     = true

Why each part:

  • ClientId is the blueprint; SignedAssertionFilePath tells the sidecar to authenticate as the blueprint with the token in the file. On Azure the same setting would point at a managed identity; this is the only identity difference between the two clouds.
  • The a365obs entry is the downstream API the agent needs an agent identity token for.
  • AllowWebApiToBeAuthorizedByACL lets app-only callers through validation; the agent then authorizes them by application id.
  • Bind to 127.0.0.1 and give the sidecar no port mapping. Its token endpoint is unauthenticated by design and must be reachable only inside the task.

3.3 Start the containers in the right order

In the ECS task definition:

  • token-helper: has a health check that passes once the token file exists and is not empty.
  • auth-sidecar: dependsOn the token helper being HEALTHY; mounts the shared volume read-only.
  • agent: dependsOn the sidecar having started.

Why: the sidecar cannot authenticate until the first token has been written. Without this ordering the first requests after a deployment fail.

3.4 Validate the caller: GET /Validate

resp = httpx.get(f"{SIDECAR_URL}/Validate",
                 headers={"Authorization": request.headers["authorization"], "Host": "localhost"})
if resp.status_code == 401:
    raise HTTPException(401, "invalid token")
claims = resp.json()["claims"]

scopes = str(claims.get("scp", "")).split()
caller_app = str(claims.get("azp") or claims.get("appid") or "").lower()
if "access_agent" in scopes:                                # a user, through an approved client
    pass
elif not scopes and caller_app in ALLOWED_CALLER_APP_IDS:   # an allow-listed application
    pass
else:
    raise HTTPException(403, "caller is not authorized for this agent")

Why: the sidecar checks signature, issuer, tenant, expiry and audience (the blueprint). The agent only decides what validated claims may do. This is the first thing on every request.

3.5 Get a token as the agent: GET /AuthorizationHeaderUnauthenticated/{api}

def sidecar_token(api_name: str, agent_identity_id: str) -> str:
    resp = httpx.get(f"{SIDECAR_URL}/AuthorizationHeaderUnauthenticated/{api_name}",
                     params={"AgentIdentity": agent_identity_id},
                     headers={"Host": "localhost"})
    resp.raise_for_status()
    return resp.json()["authorizationHeader"].split(" ", 1)[1]

What happens behind this one call:

  1. The sidecar reads the AWS-signed token from the file.
  2. It presents it to Entra as the blueprint’s client assertion and receives a blueprint token.
  3. It exchanges that for a token for the agent identity, scoped to the downstream API.

Decoding the result shows an Entra token whose oid is the agent identity and whose parent application is the blueprint: an Entra identity token, obtained by a container on AWS, with no secret. Cache it until a few minutes before it expires.

3.6 Call the model

from langchain_aws import ChatBedrockConverse

llm = ChatBedrockConverse(model="<inference-profile-id>", region_name="<region>")

No key appears here either: the AWS SDK uses the task role’s credentials.

3.7 What a caller does

TOKEN=$(az account get-access-token --scope "api://<blueprint-app-id>/access_agent" --query accessToken -o tsv)

curl -s -X POST "https://<agent-endpoint>/chat" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"message": "What time is it in UTC?", "session_id": "demo"}'

Without the header the agent returns 401. A user outside the group never gets a token (AADSTS50105). If the agent has no public endpoint, make the call from inside the VPC.


Phase 4: Observe every request (Agent 365 observability SDK)

This phase is identical to an Azure-hosted agent. Hosting on AWS changes nothing in the code.

4.1 Install and pin

microsoft-opentelemetry[langchain]

Pin exact versions of this package and of langchain, langchain-core, langgraph and langchain-aws, and re-test when you bump them. The LangChain instrumentation is version-sensitive, and when it does not attach it fails silently.

4.2 Switch telemetry on, before the app objects exist

from microsoft.opentelemetry import use_microsoft_opentelemetry

use_microsoft_opentelemetry(
    resource=Resource.create({
        "service.name": AGENT_NAME, "service.version": APP_VERSION,
        "cloud.provider": "aws", "cloud.platform": "aws_ecs", "cloud.region": AWS_REGION,
    }),
    enable_a365=True,
    a365_enable_observability_exporter=True,
    a365_use_s2s_endpoint=True,            # app-only token of the agent identity
    a365_token_resolver=token_resolver,    # see 4.3
    enable_sensitive_data=True,            # include prompts and responses
    instrumentation_options={"langchain": {"enabled": True}},
)

Why each setting:

  • enable_a365 and the exporter send spans to Agent 365, which routes them to Purview, Defender and the Microsoft 365 admin center. The agent never calls Purview directly.
  • a365_use_s2s_endpoint=True means the agent authenticates as itself, not on behalf of a user.
  • enable_sensitive_data=True is the governance decision in this file. Without it prompts and responses are not recorded, and compliance tooling has nothing to inspect. With it, treat the telemetry as sensitive data.
  • The LangChain option creates the model (chat) and tool (execute_tool) spans automatically, including for the Bedrock model.

Order: call this before creating the web app and the LangChain objects. Objects created earlier are not instrumented.

4.3 Give the exporter a token: the token resolver

def token_resolver(agent_id: str, tenant_id: str) -> str | None:
    with suppress_instrumentation():          # do not trace the token request itself
        return sidecar_token("a365obs", agent_id)

Why: the exporter authenticates to Agent 365 as the agent identity. This is where the whole chain is exercised end to end: AWS token, federated credential, blueprint token, agent identity token, OtelWrite role.

4.4 Wrap each request in an invoke_agent span

from microsoft.opentelemetry.a365.core import (
    AgentDetails, BaggageBuilder, CallerDetails, Channel,
    InvokeAgentScope, InvokeAgentScopeDetails, Request, UserDetails)

AGENT = AgentDetails(agent_id=AGENT_IDENTITY_ID, agent_name=AGENT_NAME,
                     agent_blueprint_id=BLUEPRINT_APP_ID, tenant_id=TENANT_ID,
                     agent_version=APP_VERSION, provider_name="LangChain")

baggage = (BaggageBuilder()
           .tenant_id(TENANT_ID).agent_id(AGENT_IDENTITY_ID).agent_blueprint_id(BLUEPRINT_APP_ID)
           .conversation_id(conversation_id).session_id(session_id)
           .user_id(claims["oid"]).user_email(claims.get("preferred_username"))
           .build())
request = Request(content=message, session_id=session_id,
                  conversation_id=conversation_id, channel=Channel(name="http-api"))
caller = CallerDetails(user_details=UserDetails(user_id=claims["oid"],
                                                user_email=claims.get("preferred_username")))

with baggage, InvokeAgentScope.start(request, InvokeAgentScopeDetails(), AGENT, caller) as scope:
    scope.record_input_messages(message)
    result = agent.invoke({"messages": [{"role": "user", "content": message}]}, config)
    scope.record_output_messages(reply_text)

Why:

  • InvokeAgentScope is the root span: “this caller invoked this agent with this request”.
  • BaggageBuilder copies the identity attributes onto every child span, so each model and tool call is attributed to the same agent, tenant, conversation and caller.
  • The caller details come from the claims validated in step 3.4, so the audit trail names an authenticated person.

4.5 Check that Agent 365 accepts the telemetry

# At startup: does this tenant accept telemetry from this agent?
httpx.get(f"https://agent365.svc.cloud.microsoft/observabilityService/tenants/{TENANT_ID}/eligibility?api-version=1",
          headers={"Authorization": f"Bearer {token}"})

Also set the logger microsoft.opentelemetry.a365.core.exporters.agent365_exporter to DEBUG. Each export then logs the HTTP status and a per-destination result ("status": "sent") in the task’s CloudWatch logs. That is the direct evidence that telemetry left AWS and was accepted. Data can take several minutes longer to appear in the Defender and Purview portals.

4.6 Where it shows up

PlaceWhat to look for
Microsoft 365 admin center > AgentsThe agent’s registry entry and activity
Microsoft Purview > DSPM > AI observability, and AuditInteractions, prompts and responses, by agent and by user
Microsoft Defender > Advanced huntingCloudAppEvents with action types InvokeAgent, InferenceCall, ExecuteToolBySDK
CloudAppEvents
| where Timestamp > ago(1d)
| where ActionType in ("InvokeAgent", "InferenceCall", "ExecuteToolBySDK")
| where tostring(RawEventData) has "<agent-identity-id>"
| order by Timestamp desc

The order, and what breaks if you change it

#CallMust come afterIf skipped or out of order
1Enable AWS outbound identity federation—No issuer; the task cannot get a signed token
2Create the task role (sts:GetWebIdentityToken, Bedrock)1No subject for Entra to trust
3Create blueprint—Nothing else in Entra can be created
4Add API scope, then pre-authorize clients3Callers cannot request a token
5Create blueprint principal3No agent identity, no access assignments
6Add federated credential (AWS issuer + role ARN)1, 2, 3The sidecar cannot authenticate as the blueprint
7Create agent identity5The agent has no identity to act as
8Register in Agent 3653, 7Not in the inventory, or not linked to its identity
9Owners; assign users group and callers5, 7No accountable owner; nobody can be granted access
10Assignment required9Before 9: everyone is locked out. Never: anyone in the tenant can get a token
11OtelWrite for the agent identity7Telemetry is rejected; tokens cached earlier lack the role
12Start the task: helper, then sidecar, then agent6, 7, 11First requests fail until the token file exists
13use_microsoft_opentelemetrybefore app objects are createdModel and tool spans are missing
14Per request: /Validate, then the invoke_agent scope, then the model call12, 13Unauthenticated use, or activity with no caller attached

Common problems

SymptomCauseFix
Sidecar cannot get a token; Entra reports no matching federated identity recordIssuer, subject or audience differs from the AWS token (often case, or a renamed role)Compare the helper’s logged iss/sub/aud with step 1.4
sts:GetWebIdentityToken is deniedFederation not enabled, or the audience condition does not matchSteps 0.1 and 0.2
Entra rejects the assertionToken not RS256, or more than one audienceRequest it as in step 3.1
Sidecar fails at startup, then recoversIt started before the first token was writtenContainer ordering, step 3.3
Request_ResourceNotFound right after creating an Entra objectReplication delayRetry for up to a minute
Sign-in fails with AADSTS50105Caller is not assigned to the blueprint principalAdd them to the group
Exports rejected, or eligibility returns 403OtelWrite missing, or token cached before the grantStep 2.4, then restart the task
Agent works but there are no chat / execute_tool spansLangChain instrumentation did not attachPin versions; call telemetry setup first
Bedrock returns an access error about use-case detailsThe model provider requires a one-time form per accountSubmit it, or choose another model

For comparison: listing native AWS agents without the SDKs

Agent 365 can also discover agents built directly in Amazon Bedrock (Agents and AgentCore) through Connected platforms in the Microsoft 365 admin center (Agents > All Agents > Connected platforms > Connect a platform > Amazon Bedrock). It reads agent metadata from AWS with a read-only identity that needs:

ec2:DescribeRegions
bedrock:ListAgents, bedrock:GetAgent
bedrock-agentcore:ListHarnesses, bedrock-agentcore:GetHarness
bedrock-agentcore:ListAgentRuntimes, bedrock-agentcore:GetAgentRuntime

The two approaches answer different questions:

Registry sync (Connected platforms)SDK integration (this article)
Agent appears in the Agent 365 inventoryYesYes
Own Entra agent identity, owner and sponsorNoYes
Entra decides who may call the agentNoYes
Activity and audit trailOptional: Agent 365 can read activity from the agent’s AWS logs once that is configured on the AWS sideYes: every request, model call and tool call is sent by the agent, attributed to its Entra identity and to the authenticated caller
Code changes to the agentNoneA sidecar, a small helper and a telemetry module
Credential usedA long-lived AWS access key held by Agent 365None stored anywhere

Sync is the quick way to see what exists. The SDK path is what makes an agent governed. They work well together: sync for discovery, SDK integration for the agents that matter.

What this gives you

  • No secrets, in either cloud. AWS signs a short-lived token about the task; Entra trusts it for one role and one audience.
  • A real Entra identity for an AWS workload, with a sponsor and an owner, that can be reviewed, restricted or disabled like any other identity.
  • Access control enforced by Entra before a request reaches AWS.
  • One audit trail in Purview and Defender for agents wherever they run.

About 40 lines of helper code and one federated credential are the whole AWS-specific part. Everything else is the same as for an agent hosted on Azure.

Discussion

Comments are powered by Giscus / GitHub Discussions. They appear here once configured — see Configure Giscus in the project README and update GISCUS in src/consts.ts.