AWS STS ExternalId and the Confused Deputy Problem: A Practical Guide
Cross-account IAM roles are the standard mechanism for granting third-party services access to your AWS account. You create a role, write a trust policy that names the vendor's AWS account as the trusted principal, and hand them the role ARN. The vendor's service calls sts:AssumeRole and operates within your account under scoped permissions.
The problem is that naming an entire AWS account as the trusted principal is not specific enough. Any identity authenticating from that account — including other customers of the same vendor — can call sts:AssumeRole on your role. This is the confused deputy problem, and ExternalId is the condition that closes it.
What the Confused Deputy Problem Is
The confused deputy is a class of security vulnerability where a privileged program (the deputy) is tricked by a less-privileged caller into performing actions on the caller's behalf. The deputy has legitimate authority. The attacker has no direct authority. But the attacker can cause the deputy to exercise its authority in a way that benefits the attacker.
In the AWS context: a SaaS provider (the deputy) has a trusted AWS account. Customers create cross-account roles that name the SaaS provider's account as the trusted principal. Any identity in the SaaS provider's account can call sts:AssumeRoleon any customer's role — because from AWS's perspective, the call is originating from the correct trusted account. The SaaS provider's account is the confused deputy: it has the authority, and anyone who can authenticate from that account can exploit it to reach another customer's resources.
A Concrete Example
MonitoringCo is a SaaS that monitors AWS infrastructure. Their AWS account ID is 111111111111. Customer A creates a role in their account to grant MonitoringCo read access. The trust policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}
]
}Customer B is also a MonitoringCo customer. They also authenticate from account 111111111111. If Customer B discovers Customer A's role ARN — through a misconfigured dashboard, a leaked configuration file, or simply by guessing account IDs — they can call sts:AssumeRoleon Customer A's role. AWS sees a request from the trusted principal (111111111111) and approves it.
Customer B now has all the permissions that Customer A granted to MonitoringCo inside Customer A's account. If those permissions include read access to S3 buckets, CloudWatch logs, RDS snapshots, or DynamoDB tables, Customer B can access all of it. MonitoringCo's account was the confused deputy: it carried the trust relationship, and any tenant of that account could exploit it.
ExternalId: The Mitigation
ExternalId is a condition value that the customer embeds in their trust policy. The SaaS provider must supply the matching value when calling sts:AssumeRole. If the value does not match — or is absent — the call is denied.
The ExternalIdacts as a shared secret scoped to a single customer relationship. MonitoringCo generates a unique identifier for Customer A at onboarding time and stores it in their database alongside Customer A's role ARN. When MonitoringCo calls sts:AssumeRole on Customer A's role, they pass Customer A's ExternalId. When Customer B tries the same call on Customer A's role, they would need Customer A's ExternalId, not their own. They don't have it, and the call is denied.
The secure trust policy adds a condition:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "a3f7b2c9-unique-per-customer-id"
}
}
}
]
}Now the trust policy enforces both conditions: the request must come from the right AWS account and supply the correct ExternalId. Other tenants of MonitoringCo's account cannot satisfy the second condition.
The ExternalId Call
On the SaaS provider side, the AssumeRole call includes the ExternalId as a parameter. This is the AWS SDK call MonitoringCo makes when collecting data for Customer A:
{
"RoleArn": "arn:aws:iam::CUSTOMER_ACCOUNT:role/MonitoringRole",
"RoleSessionName": "monitoring-session",
"ExternalId": "a3f7b2c9-unique-per-customer-id"
}AWS evaluates the condition in the trust policy against the value in the request. If they match, the assume-role succeeds and a temporary credential is returned. If they do not match — or if ExternalId is omitted — the request is denied with an AccessDenied error. No error message reveals whether the role exists or what the correct ExternalId value is.
ExternalId Best Practices
Getting ExternalIdright requires attention to a few implementation details. The value's security properties depend on how it is generated, stored, and transmitted.
- Generate a UUID per customer relationship at onboarding time. The
ExternalIdshould be unique to the pairing of the SaaS provider and the specific customer. A UUID v4 is the standard choice — cryptographically random and collision-resistant. Store it in your database linked to the customer's record and role ARN. - Use an unguessable value. A sequential customer ID, an account ID, or a predictable hash is not sufficient. If an attacker can predict or enumerate the
ExternalIdvalues for other customers, the mitigation fails. Use a random UUID v4 or an equivalent source of entropy. - Never reuse ExternalId values across customers. Each customer relationship must have its own unique value. Sharing values across customers would allow any customer who learns their own
ExternalIdto attempt access to other roles that share it. - Transmit it over a secure channel. The
ExternalIdis sensitive. Deliver it to the customer through your onboarding flow over HTTPS. Do not embed it in public documentation, sample policies, or unencrypted email. The customer pastes it into their trust policy — this is the only moment it needs to cross a boundary. - Missing ExternalId is a security bug when the principal is a shared account. Any cross-account trust policy that names a shared external account root and lacks an
sts:ExternalIdcondition is vulnerable to the confused deputy attack. This applies to every third-party integration that uses this pattern.
When You Do Not Need ExternalId
ExternalId only addresses the confused deputy problem, which only arises when the trusted principal is a shared AWS account used by multiple parties. If the trust policy names a specific IAM role ARN inside another account you control — for example, a CI/CD role in a deployment account — there is no confused deputy risk. The principal is already scoped to a single identity that only your systems control.
That said, there is still a best practice for internal cross-account roles: always pin the exact role ARN rather than trusting the account root. A trust policy that names arn:aws:iam::ACCOUNT:root grants access to any identity in that account, which is broader than necessary. Pin it to arn:aws:iam::ACCOUNT:role/specific-role-name instead. This is not the same problem as the confused deputy, but it follows the same least-privilege principle.
The short rule: if the trusted principal is an external account you do not fully control (a vendor, a SaaS, a partner), require ExternalId. If the trusted principal is a specific role ARN inside an account you own, pin the ARN and skip ExternalId.
Finding Missing ExternalId in Your Trust Policies
Auditing for this vulnerability requires examining role trust policies, not permission policies. The pattern to look for is a trust policy that meets all three conditions:
- Principal is an external account root or wildcard. Patterns like
arn:aws:iam::EXTERNAL_ACCOUNT:rootorarn:aws:iam::EXTERNAL_ACCOUNT:*indicate the trust is open to any identity in that account. - Action is sts:AssumeRole. The role is designed for cross-account assumption, which is the precondition for the confused deputy attack.
- No sts:ExternalId condition. The trust policy allows assumption with no per-customer secret required.
Roles meeting all three criteria should be reviewed against the list of services that have access to them. If the trusted account belongs to a SaaS provider that has multiple customers, the risk is high. If the trusted account is a single-purpose internal account you own, the risk profile is lower but still worth reviewing.
Severity is highest when the permissions granted to the role are broad — particularly read access to sensitive data stores, or write access to any resource. Even a read-only cross-account role without ExternalId is a data-exposure risk if the resource it can read contains customer data.
Catch cross-account trust policy risks before they ship
Paste an IAM policy or trust policy and get AI-Powered analysis in seconds — free, no credit card.
Amazon Web Services (AWS) is a trademark of Amazon.com, Inc. Shieldly is not affiliated with, endorsed by, or sponsored by Amazon Web Services.