[agenticwork]
← blog

Run as the user, not the robot: Entra OBO identity across AWS, GCP, and Kubernetes

A privileged action taken by an AI agent has to be attributable to the human who authorized it — not to a shared service account that a dozen people quietly share. Standing keys and shared service principals make that attribution impossible: they collapse every operator into one anonymous machine identity. The fix is an identity model where the agent runs as the Entra user, end to end — the human signs in once, and that identity federates down into every cloud and infrastructure plane the agent touches.

The problem: a shared key is a blind spot

Shared service accounts and long-lived access keys collapse N humans into one machine identity. When the agent acts through a single role, your audit trail tells you that AgentServiceRole did something — never that Alice did it. The action happened; the person is gone. For most apps that's an annoyance. For a high-impact system it's a finding.

It is a finding because it violates the control families these systems are held to. Under NIST SP 800-53 and the FedRAMP High baseline:

  • AC-2 (account management) expects accounts to map to individuals, not to a pool behind one key.
  • AC-6 (least privilege) rules out a broad standing grant that every tool inherits.
  • AU-2 / AU-12 (audit events) require each event to be attributable to a specific user.
  • IA-2 (identification & authentication) wants individual identification anchored in phishing-resistant MFA.

The honest part: a standing key is operationally easy, and that is exactly why it is everywhere — and exactly why it fails an auditor. Convenience and attribution pull in opposite directions, and the key wins until someone has to explain who did what.

The model: run as the Entra user, not as a robot

The model is on-behalf-of, or OBO (the agent exchanges the human's own Entra ID token for a short-lived, downstream credential scoped to that human — so the platform only ever has the access you hand it, never a standing key of its own). Entra (Azure AD / Entra ID) is the identity provider. The human signs in once to Entra; their identity federates down into each cloud and infrastructure plane, so privileged actions execute as the Entra user with short-lived credentials — never a shared key.

This is grounded in real code, not a slide. In the open-source core the AWS path is implemented: an Azure AD access token is exchanged via STS AssumeRoleWithWebIdentity for short-lived AWS credentials, the user's email is stamped into the RoleSessionName so CloudTrail records the human, and the credentials are cached briefly and expire (one hour by default, served with a 60-second safety buffer so a call never runs on an expiring token). One detail matters end to end: it is the access token, not the id token, that STS, Azure ARM, and GCP IAM accept as the bearer. And the credentials are minted per user, per session — least-privilege scoped, not one shared role for everyone.

The requirement that drove this: end to end, as the user

This design didn't start as a feature idea. It started as a hard requirement from the kind of operators it was built for: every action in the platform has to be auditable end to end as the human who is logged in — not just the final cloud API call. The chat message that asked for it, the Flow that ran it, the Code Mode session that wrote it, and every tool call in between all have to resolve to the same person. A platform persona that quietly maps to a shared service account fails that test on the first question an auditor asks.

Running as the Entra user is what makes that continuity possible — and it has a second payoff that matters just as much: the platform inherits the cloud RBAC already assigned to that identity. There is no second permission model to build, reconcile, and watch drift out of sync. What you can do and see in the platform is exactly what your identity is already allowed to do and see in the cloud, because the platform is acting as that identity — not translating it through a mapping table of its own.

That lines up with how privileged access already works in a well-run shop. Operators commonly carry two identities: a standard user for day-to-day work, and an elevated user — a privileged or just-in-time admin role, the directory equivalent of sudo — for the operations that need it. Which identity you sign into the platform as determines what you can do and see. Sign in as your standard user and you get standard scope; sign in as your elevated user and you get the elevated scope that identity carries. It behaves exactly as it would if you logged into the Azure portal, the AWS console, or gcloud directly as that user — because, underneath, that is precisely what is happening. The platform isn't a new front door with its own keys; it's you, at the same door, with the same key you already hold.

The result is one continuous chain of attribution. The platform's own append-only audit log and each cloud's native log — CloudTrail, Cloud Audit Logs, the Kubernetes audit log — name the same human, from the first chat message down to the last API call, at whichever privilege level that human signed in with.

Federating into each plane

The same shape repeats across all three target planes: an Entra token goes in, short-lived per-user credentials come out, and the plane's native audit log records the individual. AWS is implemented today; GCP and Kubernetes are the paths the architecture is built to follow.

AWS — STS AssumeRoleWithWebIdentity

Entra federates into AWS through IAM Identity Center (SAML or OIDC trust). The agent presents the user's Entra access token to STS, which returns short-lived credentials tied to that user, and the RoleSessionName carries the human's email — so CloudTrail attributes the action to the person, not the role. This is the implemented path (the AWS OIDC federation module plus a per-user Bedrock client). To be precise about its current shape: it is role-per-user-per-session today, not role-per-tool.

GCP — Workload Identity Federation

The same pattern maps onto GCP through Workload Identity Federation: Entra OIDC into GCP's STS for short-lived Google credentials, with claim mapping (the Entra oid / email becomes the Google principal) so Cloud Audit Logs attribute the human. This is the path the architecture is built for. Today the Vertex auth reads Application Default Credentials and falls through to Workload Identity on a GCP node; the per-user Entra token exchange is the designed extension of that, not yet wired the way AWS is.

Kubernetes — OIDC issuer to RBAC as the Entra user

For Kubernetes, Entra acts as the cluster's OIDC provider (AKS exposes this natively; on EKS/GKE you point the API server at the issuer with --oidc-issuer-url and --oidc-client-id). The user's Entra group or oid maps to a Role / ClusterRoleBinding, so the Kubernetes audit log shows the human — a ClusterRoleBinding that resolves to the Entra user. This too is built for: the MCP manager already detects in-cluster auth and passes KUBERNETES_SERVICE_HOST / KUBERNETES_SERVICE_PORT through a FedRAMP SC-4 filtered environment to MCP subprocesses, but the explicit Entra-OIDC-to-RBAC wiring is the designed model rather than something fully wired in the current snapshot.

What it buys you: the human in every log

The payoff is one sentence with a lot behind it: CloudTrail (AWS), Cloud Audit Logs (GCP), and the Kubernetes audit log all name the individual, never a shared robot. Mapped back to the controls — AC-6 is satisfied by short-lived, least-privilege credentials instead of a broad standing grant; AU-2 / AU-12 are satisfied by per-user attribution in every plane's native log; and IA-2 is satisfied because identity is anchored in Entra's phishing-resistant MFA.

The open-source core's own audit layer closes the loop. Every agent tool call is written to an append-only audit log (the agent_audit_log table records each action_typetool_call, llm_completion, delegation, approval, data_access — keyed to the acting user_id), and the admin event trail is tamper-evident via per-row hash chaining. Tool calls pass through an approval gate (approvalGate) before anything mutating runs. When the tool itself executes as the Entra user, that internal record and the cloud's native audit log agree on one name — the human — from the chat message all the way down to the API call.

Built to meet FedRAMP High and NIST 800-53 high-impact

Federal, healthcare, defense, and regulated industries are the environments this identity architecture is built for, precisely because they hold systems to the high-impact identity and audit controls that OBO run-as-user is designed to satisfy.

To be exact about what that claim is: this describes a capability and a design intent, not a certification. Compliance is a property of a specific deployment and its authorizing body — not of software in the abstract. The architecture is built to meet and architected to satisfy FedRAMP High and NIST 800-53 high-impact identity and audit controls; it does not carry an authorization, and no deployment is implied by anything written here.

OSS vs. enterprise: the identity seam

The split between the self-hosted build and enterprise sits right at the identity source. The open-source core stays free to self-host either way; what changes is which identity providers it can anchor to and how cloud MCP credentials are minted.

IdentityOSS (self-hosted)Enterprise
Primary IdPEntra ID as the supported IdP — the single-IdP identity architecture the platform is built aroundEntra plus other primary cloud IdPs (AWS IAM Identity Center / SSO, Google Workspace / Cloud Identity)
Cloud MCP credsToday's public self-hosted build uses local-auth and static service-principal creds for cloud MCP servers; OBO run-as-user is the regulated identity model the platform is built around, and the install wizard is gaining an Entra optionPer-user OBO across the supported IdPs
AttributionShared role (local-auth default)Per-user OBO — the human in every log

To be straight about the current public tarball: Entra OBO is the supported identity architecture the platform is built for, with the AWS OBO path implemented in the Bedrock model-provider client (the per-user AssumeRoleWithWebIdentity exchange above) — not a finished, flip-it-on toggle in today's local-auth OSS release. The cloud MCP servers (aws, azure, gcp) are a separate surface: in the self-hosted build they authenticate with static service-account credentials, and per-user OBO for those MCP servers is the enterprise/regulated identity model the platform is built around — see the MCP servers and Configuration docs.

Setting it up

Here is the wiring, plane by plane. Every block below is standard vendor syntax — the only values specific to the open-source core are the two environment variables the AWS module actually reads. Don't expect product-specific flags beyond those.

1. Register the Entra app + expose the OBO scope

In Entra ID under App registrations, create (or reuse) the open-source core's app registration and note its Application (client) ID and Directory (tenant) ID. Under Expose an API, set the Application ID URI (e.g. api://<client-id>) and add a delegated scope (e.g. access_as_user) — that is the scope the agent requests on the user's behalf. Under API permissions, add the delegated downstream permissions the agent will exercise, then grant tenant admin consent.

The on-behalf-of grant itself: the platform receives the user's access token (not the id token — that is what STS, ARM, and GCP IAM accept as a bearer) and exchanges it downstream. In Entra the OBO flow uses grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer with requested_token_use=on_behalf_of against the v2.0 /token endpoint — see Microsoft's on-behalf-of flow docs.

Takeaway: Entra is now the single identity anchor; every downstream exchange starts from the user's own token.

2. AWS — trust Entra at IAM Identity Center / STS

Federate Entra into AWS in one of two supported shapes: (a) SAML / OIDC into IAM Identity Center for console access and permission sets, or (b) a direct OIDC identity provider in IAM for the token-to-STS exchange. For the run-as-user STS path, create an IAM role (the OBO role) whose trust policy trusts the Entra OIDC issuer and constrains the audience:

{
 "Version": "2012-10-17",
 "Statement": [{
 "Effect": "Allow",
 "Principal": { "Federated": "arn:aws:iam::<account-id>:oidc-provider/login.microsoftonline.com/<tenant-id>/v2.0" },
 "Action": "sts:AssumeRoleWithWebIdentity",
 "Condition": { "StringEquals": {
 "login.microsoftonline.com/<tenant-id>/v2.0:aud": "<entra-client-id>"
 }}
 }]
}

Attach a least-privilege permission policy to that role — only what the agent's tools need. Then point the open-source core at it. The AWS OIDC federation module resolves the role ARN in order: an explicit roleArn option, the env var AWS_OBO_ROLE_ARN (a full ARN), or AWS_ACCOUNT_ID (from which it constructs arn:aws:iam::<account>:role/AgentOBORole):

export AWS_OBO_ROLE_ARN="arn:aws:iam::123456789012:role/AgentOBORole"
# or: export AWS_ACCOUNT_ID="123456789012"

At call time the platform runs AssumeRoleWithWebIdentity with the user's Entra access token as the WebIdentityToken and a RoleSessionName derived from the user's email (sanitized, capped at 32 chars) — so CloudTrail records the human. The credentials are short-lived (one hour by default).

Takeaway: this AWS path is implemented in code today — every Bedrock call can run as the signed-in Entra user.

3. GCP — Workload Identity Federation pool + provider

Create a Workload Identity Pool and an OIDC provider that trusts Entra:

gcloud iam workload-identity-pools create agent-pool \
 --location=global --display-name="Entra Federation"

gcloud iam workload-identity-pools providers create-oidc entra \
 --location=global --workload-identity-pool=agent-pool \
 --issuer-uri="https://login.microsoftonline.com/<tenant-id>/v2.0" \
 --allowed-audiences="<entra-client-id>" \
 --attribute-mapping="google.subject=assertion.sub,attribute.email=assertion.email"

Grant the federated principal least-privilege access — bind the mapped attribute to the roles the agent needs, optionally via service-account impersonation:

gcloud iam service-accounts add-iam-policy-binding <sa>@<project>.iam.gserviceaccount.com \
 --role=roles/iam.workloadIdentityUser \
 --member="principal://iam.googleapis.com/projects/<num>/locations/global/workloadIdentityPools/agent-pool/subject/<entra-subject>"

The platform exchanges the Entra OIDC token for short-lived Google credentials, and Cloud Audit Logs then attribute the human — see the Workload Identity Federation docs. To be honest about the current code: the Vertex auth today reads GOOGLE_APPLICATION_CREDENTIALS_JSON / falls through to Application Default Credentials or Workload Identity on a GCP node; the per-user Entra token exchange above is the path the architecture is built for, not something shipping in that form yet.

Takeaway: same pattern as AWS — Entra token in, short-lived Google creds out, the human in the log.

4. Kubernetes — API-server OIDC + a ClusterRoleBinding

Configure the API server to trust Entra as an OIDC issuer (AKS exposes this natively; on EKS/GKE you supply an OIDC issuer config). Illustrative kube-apiserver flags:

--oidc-issuer-url=https://login.microsoftonline.com/<tenant-id>/v2.0
--oidc-client-id=<entra-client-id>
--oidc-username-claim=email
--oidc-groups-claim=groups

Then bind the Entra group (or a specific user oid) to RBAC — this is the “ClusterRoleBinding as the Entra user”:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 name: agent-operators
roleRef:
 apiGroup: rbac.authorization.k8s.io
 kind: ClusterRole
 name: edit # least-privilege built-in or a custom Role
subjects:
- apiGroup: rbac.authorization.k8s.io
 kind: Group
 name: "<entra-group-object-id>" # Entra group GUID from the groups claim

With OIDC plus this binding, kubectl and agent calls authenticate as the Entra user, and the Kubernetes audit log records the individual — see the Kubernetes OIDC docs. As with GCP: the open-source core's MCP manager already auto-detects in-cluster auth and passes only a FedRAMP SC-4 filtered environment to MCP subprocesses, but the explicit Entra-OIDC-to-RBAC wiring is the designed model rather than something shipping today.

Takeaway: Entra group, RBAC binding, the k8s audit log names the human — the same principle as the cloud planes.

Anchor identity in Entra. Federate it down.

Anchor identity in Entra. Federate it down. Let every log name the human — not a robot, not a shared key. The AWS path is implemented today; GCP and Kubernetes are the planes the same architecture is built to reach. Read the architecture, then wire it.

Read the architecture →

And when you're ready to run it, the operator's next step is one line:

curl -sSL https://install.agenticwork.io | bash