This check flags IAM roles that other AWS accounts can assume without an external ID or MFA condition, which opens the door to the confused deputy problem. Fix it by adding a sts:ExternalId condition for third-party roles or an aws:MultiFactorAuthPresent condition for human roles in the trust policy.
Cross-account IAM roles are the backbone of how AWS accounts talk to each other. They let a monitoring vendor read your CloudWatch metrics, let your CI account deploy into production, or let your security tooling scan resources across an organization. The problem is that the trust policy controlling who can assume a role is easy to write too loosely. When a role trusts another account but adds no further guardrails, anyone who can assume a principal in that account can step into your role.
The Cross-Account Role Lacks External ID or MFA check looks at the trust policies (the AssumeRolePolicyDocument) on your IAM roles and flags any role that grants sts:AssumeRole to an external account principal without a matching sts:ExternalId or aws:MultiFactorAuthPresent condition.
What this check detects
The check parses each role's trust policy and identifies statements that:
- Allow the
sts:AssumeRoleaction - Have a
Principalreferencing an AWS account, role, or user outside the current account (or a wildcard) - Contain no
Conditionblock enforcingsts:ExternalIdoraws:MultiFactorAuthPresent
Here is a trust policy that would trip this check. It trusts an external account but adds no conditions at all:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222233334444:root"
},
"Action": "sts:AssumeRole"
}
]
}
Note: The :root in a principal does not mean the root user. It means any principal in account 222233334444 that has IAM permissions to call sts:AssumeRole on this role. That scope is broader than most people expect.
Why it matters
The classic risk here is the confused deputy problem. Picture a SaaS vendor, call them AcmeMonitor, that asks you to create a role trusting their AWS account so they can read your billing data. Their account ID is shared with every customer. If AcmeMonitor creates the same kind of role for thousands of customers and trusts only their own account, then any AcmeMonitor customer who can convince AcmeMonitor's software to assume a role can potentially target a role in another customer's account, as long as they can guess or learn the role ARN.
The external ID condition solves this. It is a secret shared between you and the vendor that must be passed when assuming the role. Without the correct value, the assume call fails. This ties the trust to a specific relationship rather than to a whole shared account.
For human-assumed roles the risk is different but just as real. A cross-account role that does not require MFA means a single set of leaked long-lived credentials in the trusting account is enough to pivot into your account. Credentials leak constantly through committed .env files, exposed CI logs, and phished engineers. Requiring MFA on the assume call raises the bar significantly.
Warning: A wildcard principal ("AWS": "*") combined with no conditions effectively makes the role assumable by the entire internet. This is one of the highest severity findings you can have on an IAM role and should be treated as an incident, not a backlog item.
How to fix it
The fix depends on who is supposed to assume the role. Pick the path that matches the use case.
1. Third-party or service-to-service roles: add an external ID
Update the trust policy to require an external ID. The vendor supplies the value, or you generate one and give it to them. Use something long and random, not a customer name or a guessable string.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222233334444:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "a7f3c9e1-4b2d-48a6-9f1e-d3c5b8e0a142"
}
}
}
]
}
Apply it with the CLI. Save the policy above to trust-policy.json first:
aws iam update-assume-role-policy \
--role-name AcmeMonitorAccess \
--policy-document file://trust-policy.json
Danger: update-assume-role-policy replaces the entire trust policy. If the role is in active use and you get the principal wrong, every consumer breaks immediately. Pull and review the current policy with aws iam get-role --role-name AcmeMonitorAccess --query 'Role.AssumeRolePolicyDocument' before you overwrite it, and coordinate the external ID value with the vendor in advance.
2. Human-assumed roles: require MFA
For roles that engineers assume from another account, enforce MFA on the assume call:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
]
}
You can also tighten the principal to a specific role rather than the whole account, which combines well with MFA:
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/EngineerSSO"
}
3. Terraform example
If you manage roles as code, encode the condition in the assume role policy so it cannot drift:
data "aws_iam_policy_document" "trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::222233334444:root"]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}
resource "aws_iam_role" "vendor_access" {
name = "AcmeMonitorAccess"
assume_role_policy = data.aws_iam_policy_document.trust.json
}
Tip: Store the external ID in a secrets manager or pass it as a sensitive Terraform variable rather than hardcoding it in source. Treat it as a credential, because in practice it is one.
How to prevent it from happening again
Catching a single bad role is fine. Stopping the next one from shipping is better. A few layers work well together.
Policy-as-code in CI
Run a policy check against Terraform plans before they merge. With Checkov you can write a custom check, or use OPA / Conftest with a rule like this against the planned IAM resources:
# Fail the pipeline if any cross-account role lacks ExternalId or MFA
conftest test plan.json --policy iam-trust.rego
A simple Rego rule denies any assume role policy that references an external account principal without the required condition keys. Wiring this into a pull request gate means the misconfiguration never reaches production in the first place.
Service control policies
At the org level, an SCP can prevent roles from being created or updated with overly broad trust. While SCPs cannot directly inspect a trust policy document, you can pair them with AWS Config rules for detection at scale.
AWS Config and continuous scanning
Deploy an AWS Config custom rule or use Lensix to continuously evaluate trust policies across every account. Detection-after-the-fact still matters because trust policies get edited out of band during incidents, vendor onboarding, and quick fixes that never get reverted.
Tip: Schedule a recurring scan rather than relying on a one-time audit. Cross-account trust drifts most often when someone is troubleshooting a broken integration under pressure and loosens the policy to get unblocked.
Best practices
- Scope principals as tightly as possible. Trust a specific role ARN instead of
account:rootwhenever you can. Add the condition on top, do not use it as a replacement for a narrow principal. - Always use an external ID for third parties. Any vendor that asks you to create a cross-account role without offering an external ID is not following AWS guidance. Push back.
- Require MFA for human cross-account access. Combine this with short session durations using
--duration-secondson the assume call. - Generate external IDs as random values. Never use a customer name, account number, or anything an outsider could guess.
- Audit trust relationships regularly. Roles accumulate. The vendor you trusted two years ago may no longer need access at all.
- Log and alert on assume calls. CloudTrail records every
AssumeRoleevent. Alert on assume calls from unexpected source accounts.
Cross-account roles are not the problem. Cross-account roles without conditions are. Adding an external ID or an MFA requirement is a small change that closes off an entire class of pivot and confused deputy attacks, and it costs nothing to implement.

