AWS Confused Deputy Attack: Cross-Account Role Assumption Without External ID Enforcement
Theory
Why This Matters
The confused deputy problem in AWS cross-account role assumption has caused real breaches at SaaS providers: when a vendor service was compromised, attackers used the vendor's AWS account — which was legitimately trusted by hundreds of customer accounts — to assume victim IAM roles without needing any customer-specific secret. Notably, multiple cloud security companies have discovered that their own customer integrations were vulnerable: the trust policy said "trust account X" but imposed no condition on who in account X could initiate the assumption. Any principal in account X — including an attacker who compromised an unprivileged role — could pivot into every customer's environment. External ID is not optional security theatre; it is the only cryptographic guard against a trusted third-party account being used as a stepping stone.
Core Concept
Cross-account role assumption allows a principal in one AWS account to call sts:AssumeRole on a role in a different account, provided the role's trust policy permits it. A minimal trust policy specifies "Principal": {"AWS": "arn:aws:iam::TRUSTED_ACCOUNT_ID:root"} — this means any principal in the trusted account can assume the role.
The confused deputy problem arises in multi-tenant SaaS architectures: Customer A creates an IAM role and trusts the SaaS vendor's AWS account. The vendor's service assumes that role to read customer data. But if the trust policy has no conditions, any role in the vendor's account — including a role an attacker has compromised — can assume any customer's role by simply knowing the role ARN. The attacker does not need to steal the vendor's service credentials specifically; any foothold in the trusted account suffices.
The External ID is a condition value — a shared secret established out-of-band between the customer and the vendor — that must be supplied in the AssumeRole call. The trust policy enforces it with a Condition block: "StringEquals": {"sts:ExternalId": "CUSTOMER_SPECIFIC_UUID"}. Because each customer uses a different External ID, a compromised vendor role that knows Customer A's role ARN cannot assume Customer B's role without also knowing Customer B's External ID.
Important limitation: External ID prevents confused deputy attacks but does not prevent a fully compromised vendor account from assuming customer roles — if the vendor's master credential is stolen, the attacker can retrieve all External IDs from the vendor's configuration store. External ID is a defence against cross-tenant confused deputy, not against total vendor compromise.
Technical Deep-Dive
# Enumerate all roles in the account and check trust policies for cross-account trust
# without ExternalId conditions
aws iam list-roles
--query 'Roles[*].{Name:RoleName,Arn:Arn}'
--output table
# Retrieve and inspect the trust policy of a specific role
aws iam get-role --role-name TargetCrossAccountRole
--query 'Role.AssumeRolePolicyDocument'
--output json | python3 -m json.tool
# Python script: flag all roles with cross-account trust lacking ExternalId condition
python3 << 'PYEOF'
import boto3, json
iam = boto3.client('iam')
paginator = iam.get_paginator('list_roles')
for page in paginator.paginate():
for role in page['Roles']:
doc = role['AssumeRolePolicyDocument']
for stmt in doc.get('Statement', []):
principal = stmt.get('Principal', {})
aws_principal = principal if isinstance(principal, str) else principal.get('AWS', '')
if not aws_principal:
continue
# Check if principal is a different account
role_account = role['Arn'].split(':')[4]
principals = [aws_principal] if isinstance(aws_principal, str) else aws_principal
for p in principals:
if ':' in p and p.split(':')[4] != role_account:
cond = stmt.get('Condition', {})
has_ext_id = 'sts:ExternalId' in json.dumps(cond)
flag = '' if has_ext_id else ' *** MISSING ExternalId ***'
print(f"{role['RoleName']:40s} trusted: {p}{flag}")
PYEOF
# Attempt role assumption without ExternalId (will succeed if misconfigured)
aws sts assume-role
--role-arn "arn:aws:iam::VICTIM_ACCOUNT_ID:role/VulnerableRole"
--role-session-name "confused-deputy-test"
# Attempt with ExternalId (correct pattern — should be required by trust policy)
aws sts assume-role
--role-arn "arn:aws:iam::VICTIM_ACCOUNT_ID:role/SecureRole"
--role-session-name "test-with-extid"
--external-id "customer-specific-uuid-here"
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::VENDOR_ACCOUNT_ID:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "CUSTOMER_UNIQUE_UUID"},
"Bool": {"aws:MultiFactorAuthPresent": "false"}
}
}]
}
Security Assessment Methodology
- Enumerate cross-account trust relationships. List all roles with
aws iam list-rolesand parse trust policies. Identify roles where thePrincipal.AWSbelongs to a different account ID. These are cross-account trusts. - Check each for ExternalId condition. For each cross-account trust, inspect the
Conditionblock. Any role trusting an external account without"StringEquals": {"sts:ExternalId": "..."}is potentially vulnerable to confused deputy. - Assess the trust scope. A trust of
arn:aws:iam::ACCOUNT:roottrusts every principal in that account. A trust ofarn:aws:iam::ACCOUNT:role/SpecificRoleis narrower — determine whether that specific role itself is well-protected. Broad account-root trust with no ExternalId is highest severity. - Attempt assumption from a principal in the trusted account. If you have any foothold in the trusted account (even a read-only role), call
sts:AssumeRolefor the victim role without ExternalId. Success confirms the vulnerable configuration. - Review SaaS vendor integrations. Ask the customer to identify all third-party vendor integrations that use cross-account roles. Review each vendor's documentation for whether they mandate ExternalId. Vendors that do not use ExternalId at all are deploying a known-vulnerable integration pattern.
- Remediate by updating the trust policy to require
sts:ExternalIdwith a unique per-customer value. Generate a random UUID for each customer relationship, store it in the vendor's configuration, and update all existing trust policies. Rotate values if the existing ExternalId values are predictable or reused across customers.
Common Assessment Errors
- Confusing ExternalId with a password. External ID is not secret between the customer and the trusted account — it only prevents the trusted account from being used to pivot into other customers. An attacker who has fully compromised the trusted account can enumerate all External IDs from the vendor's configuration. Document this limitation clearly in reports.
- Missing the
sts:AssumeRolein resource policies. Cross-account S3 bucket policies, KMS key policies, and Lambda resource-based policies also permit cross-account access without ExternalId semantics. These are separate attack surfaces from IAM role trust policies. - Assuming a condition on the Principal ARN is equivalent to ExternalId. A trust that specifies
arn:aws:iam::VENDOR:role/SpecificRolerather than:rootis harder to exploit but still vulnerable if that specific role can be compromised or if the vendor rotates role names. ExternalId provides an additional independent control. - Not testing from inside the trusted account. Confused deputy vulnerability is only exploitable from within the trusted account. Testers who only attempt assumption from an external, unrelated account will get an access-denied and incorrectly mark the role as secure.
- Overlooking
sts:AssumeRoleWithWebIdentityandsts:AssumeRoleWithSAML. Trust policies can also be vulnerable to confused deputy via federated identity — a SAML or OIDC trust without audience/subject conditions allows any principal from the trusted identity provider to assume the role.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0053 | Knowledge of security risk management processes | Understanding the confused deputy threat model in multi-tenant SaaS architectures and the risk of cross-account trusts without ExternalId |
| K0167 | Knowledge of system administration, network, and OS hardening techniques | Hardening IAM trust policies with ExternalId conditions and narrow Principal ARNs to prevent cross-account privilege escalation |
| S0073 | Skill in conducting vulnerability scans and recognizing vulnerabilities | Scripting IAM role enumeration to identify cross-account trusts lacking ExternalId conditions across large account inventories |
| T0144 | Conduct penetration testing as required for new or updated applications | Attempting cross-account role assumption from a foothold in the trusted account to confirm confused deputy exploitability during AWS penetration tests |
| T0395 | Write code to address security vulnerabilities | Writing correct IAM trust policy JSON with sts:ExternalId conditions and updating existing policies via iam:UpdateAssumeRolePolicy |
Further Reading
- The Confused Deputy Problem — AWS IAM Documentation, Cross-Account Role Access section (docs.aws.amazon.com/IAM)
- Attacking and Defending AWS Cross-Account Roles — Scott Piper, Summit Route blog
- AWS Security Best Practices: Cross-Account Access — Amazon Web Services Whitepaper (aws.amazon.com/whitepapers)
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.