Wildcard Principal in IAM Trust Policies: The Open Door
An IAM role trust policy with Principal set to a wildcard or an unscoped account root lets any AWS account, not just the intended partner, call sts:AssumeRole. Standard resource-based deny rules and SCPs on the target account rarely inspect who is calling from outside, so the exposure survives every internal review.
At a glance
- Unsafe setting
- IAM role trust policy Principal set to a wildcard or bare account root with no ExternalId or PrincipalOrgID condition.
- Failure trigger
- An external party discovers the role ARN and calls sts:AssumeRole from an unrelated AWS account.
- Blast radius
- Any AWS account can assume the role and inherit its permissions, including data lake reads, secrets access, or KMS decrypt rights.
- Recommended control
- Scope Principal to the exact account ARN and enforce sts:ExternalId or aws:PrincipalOrgID conditions, backed by an SCP denying unscoped trust policies.
Fix commands and configuration
Principal"Principal": {"AWS": "arn:aws:iam::VENDOR-ACCOUNT-ID:root"}ConditionThe Trap
A cross-account IAM role trust policy written with "Principal": {"AWS": "*"}, or with a bare account root ARN and no Condition block, grants sts:AssumeRole to every principal in every AWS account on the internet, not just the vendor or subsidiary account the role was built for.
The Default State
This pattern appears constantly during SaaS integration onboarding. A vendor’s setup script or Terraform module ships with the trust policy hardcoded as a wildcard because the vendor doesn’t know the customer’s account ID at template-generation time, with the intention that the customer will “tighten it later”. CloudFormation quick-start stacks for monitoring and logging tools do the same thing, defaulting to a broad principal and an optional, often-skipped ExternalId parameter. Engineers copy these templates for a proof of concept, the role gets attached to a production data pipeline, and the wildcard is never revisited because the role appears to work correctly with the intended account.
The Blast Radius
Any AWS account, anywhere, can now call sts:AssumeRole against that role ARN. If the role carries permissions like s3:GetObject on a data lake bucket, secretsmanager:GetSecretValue, or kms:Decrypt, an attacker who simply discovers the role ARN, from a leaked CloudFormation output, a GitHub commit, or a public S3 bucket policy, can assume it from their own throwaway account and pull credentials scoped to the role’s permissions. This is the classic confused deputy pattern: the trust relationship trusts a role name and account root rather than a specific caller identity, and without an ExternalId or organisation condition, there is nothing to distinguish the legitimate vendor call from an opportunistic one. CloudTrail will log the assumption as a normal, successful AssumeRole event from an unfamiliar account, which most SIEM rules don’t flag unless someone has built an explicit anomaly detection for cross-account assumption from unrecognised account IDs.
The Lead Mechanic Fix
Never leave Principal unscoped. Pin it to the exact account and require conditions: "Principal": {"AWS": "arn:aws:iam::VENDOR-ACCOUNT-ID:root"} combined with a Condition block enforcing "sts:ExternalId": "unique-shared-secret" and, where the caller is internal, "aws:PrincipalOrgID": "o-xxxxxxxxxx". Run aws iam list-roles --query 'Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS=="*"]]' across every account to find existing offenders, then audit CloudTrail for AssumeRole events where the calling userIdentity.accountId doesn’t match an approved allowlist. Enforce this permanently with an SCP denying iam:CreateRole and iam:UpdateAssumeRolePolicy unless the trust policy contains a scoped principal and an sts:ExternalId condition key.