The Two Mitigations for the Service-Account Confused Deputy in the Cloud

A confused deputy is nothing more than a privileged intermediary tricked into acting for a caller who lacks the authority itself. In the cloud, it has two distinct flavors, one leveraging a non-human identity (NHI) owned by you, the customer, and one leveraging a non-human identity (NHI) owned and operated by the CSP.

The Organizing Axis: Who Owns the Identity

Every cloud splits non-human identities into two camps.

  • Customer-managed: identities you create and attach to your own compute β€” GCP service accounts, AWS IAM roles, Azure user-assigned andmanaged identities. The confused-deputy risk is you (or a lower-privileged principal in your account) attaching an identity to an execution environment you control and thus, creating a privilege escalation route to use its permissions.
  • Provider-managed: identities the CSP creates and wields as a deputy inside your tenant β€” GCP service agents (per-product, per-project service accounts), AWS service-linked roles/service principals, Azure platform-level service principals. Sometimes they carry standing trust whose privilege can be abused, leveraging service functionality.

For more detailed information on these two types of service accounts, see my whitepaper comparing CSP-managed Identities and a blog I wrote on Azure’s Platform-Level Managed Identities.

The mitigation for privilege escalation varies between the two types of service accounts.


Category 1 β€” Mark the identity attachment

Category 2 β€” Guard the deputy (provider owns the NHI)

Service Account type

Customer-managed Service Account

CSP-managed Service Account

GCP

iam.serviceAccounts.actAs

Internal caller-auth checks β€” opaque, seen as PERMISSION_DENIED

AWS

iam:PassRole 

FAS (auto, caller-forwarded) 

Azure

…/userAssignedIdentities/*/assign/action

Internal linked-auth checks β€” opaque, seen as LinkedAuthorizationFailed

Mitigation 1 β€” Mark the Attachment (Customer-Managed Identities)

When a caller attaches an identity to an execution environment such as a VM, function, job, or container, their code inherits that identity's privileges. The control is a bind-time authorization check on the caller, scoped to the target identity, evaluated when the (identity β†’ execution-environment) association is created or changed.

It is not a runtime check. Once the identity is attached, the resource's code automatically retrieves that identity's token from the metadata endpoint without re-evaluation. The gate check is performed at attachment time, not when the already-deployed workload runs.


GCP

AWS

Azure

Attachment gate

iam.serviceAccounts.actAs

iam:PassRole

…/userAssignedIdentities/*/assign/action

Usually carried by

roles/iam.serviceAccountUser

No specific β€˜Pass Role’ Policy, usually bundled with each service policy.

Managed Identity Operator

Evaluated by

The deploy or create resource  API call 

The receiving service call (ec2:RunInstances, lambda:CreateFunction)

The ARM write operation that attaches the UAMI

None of the three gates is a standalone API β€” each check is the responsibility of the service that performs the attachment.

Lots of Privilege Escalation, Only One is Via A Confused Deputy

Attaching an identity, editing who may wield it, and minting its tokens are three distinct actions in a cloud that could lead to privilege escalation. Let's make sure we are clear on which are confused deputy issues and which ones are not.

On GCP:

  • attach: iam.serviceAccounts.actAs β€” controls which identity is bound to a resource.
  • delegate: iam.serviceAccounts.setIamPolicy on the SA β€” the write that grants the actAs edge in the first place (changing the SA's own roles on other resources is different again).
  • mint: iam.serviceAccounts.getAccessToken via roles/iam.serviceAccountTokenCreator β€” token issuance, which roles/iam.serviceAccountUser does not grant.

All three permissions above manage privilege escalation; only the first marks privilege escalation via confused deputy.

Mitigation 2 β€” Guarding the Provider's Deputy

Provider-managed identities are the other half with GCP service agents, AWS service-linked roles, and Azure platform-level managed identity. The risk is a lower-privileged caller (or another tenant) inducing that deputy to use its permissions (standing or otherwise) against a resource the caller isn't entitled to.

The mitigation here lives on the provider side: internal authorization checks that validate the true originating caller has rights on the target, optionally supplemented by customer-supplied condition keys. The sharp contrast is observability β€” AWS names and exposes its machinery; GCP and Azure hide theirs behind a denial.


GCP

AWS

Azure

Provider-internal check

caller-authorization checks

Forward Access Sessions (FAS)

linked authorization checks (LinkedAccessCheck)

Named / documented?

No β€” inferred from denials

Yes

Partly β€” ProviderHub construct + error codes

Observable?

Only on failure (PERMISSION_DENIED)

Yes β€” aws:ViaAWSService, aws:CalledVia*

Only on failure (LinkedAuthorizationFailed)

Customer lever

None. entirely a provider-side control

condition keys restricting which source account/org a principal may act for (i.e.aws:SourceAccount, aws:SourceOrgID)

None. entirely a provider-side control

AWS is the only one of the three that surfaces its confused-deputy machinery as first-class primitives and gives the customer some control. The two AWS sub-mechanisms:

  • FAS: Forward Access Sessions internally carry the original IAM principal's identity and permissions, and the downstream service rechecks that same principal and its permissions. The deputy never exceeds the caller's own rights; the downstream call succeeds only if that same caller already holds the permission on the downstream resource (e.g., the KMS key), never more.
  • Condition Keys: When a service acts via an assumed service role or service-linked role, it uses the role's permissions, and the caller's identity is not carried forward. So, on the customer, condition keys can be supplied, limiting who the global principal can act on behalf of against their resources (keys include: aws:SourceArn, aws:SourceAccount, aws:SourceOrgID, aws:SourceOrgPaths).

GCP's and Azure's internal checks stay opaque β€” you learn they exist only when one denies you. Azure's fires whenever an operation references a second resource: try to create something that joins a subnet you don't control, and ARM demands you also hold Microsoft.Network/virtualNetworks/subnets/join/action on that subnet β€” not just permission on the resource you're creating. That extra check is the confused-deputy guard: it stops a create you're allowed to make from quietly pulling in a resource you're not. You'll only ever see it as a LinkedAuthorizationFailed error.

Wrap-Up

The mitigation follows the ownership. If you own the identity, the attachment gate applies  (.actAs / iam:PassRole / 'assign/action').  If the provider owns it, the guard is theirs, with the outlier being  AWS condition keys (aws:SourceAccount, aws:SourceOrgID).