Business Challenge
A company buys a cloud cost-optimisation tool. Onboarding takes ten minutes: create a role, attach a read-only policy, set the trust policy to the vendor's account ID, send them the role ARN. The vendor's documentation supplies the exact JSON. Two people review it, and both approve, because the permissions policy is read-only and that is the part they were looking at.
What the trust policy says is "Principal": {"AWS": "222233334444"} and "Action": "sts:AssumeRole". No condition block. The reviewers read that as the vendor may assume this role.
It does not mean that. It delegates to the vendor's account. Every principal in it that their administrator chooses to grant sts:AssumeRole to β today, and every principal created there for as long as the role exists β can assume it. The company has not granted access to a product; it has trusted another organisation's IAM, permanently, with no visibility into how it is administered and no event on this side when it changes.
Eighteen months later the same company adds a CI pipeline that assumes a deployment role via GitHub's OIDC provider. The trust policy names the provider and checks nothing else. It is now assumable from any repository on GitHub. And a service role trusted by "Service": "cloudtrail.amazonaws.com", written the same week, will accept a trail configured in an account nobody here has heard of.
In each case the Principal element names a set correctly and the set is far larger than the author pictured. An account is a set of principals. A service principal is a set of everyone who can configure that service. A federated provider is a set of everyone it will issue a token to. The element that makes those sets smaller is the one all three policies left out.
Architecture
AWS's own framing is worth taking literally: a role trust policy is a required resource-based policy that is attached to a role in IAM, and the trust policy is attached to the role in the trusting account, and is one-half of the permissions. Half. Not a setting on the role β one of the two documents that together decide whether anything happens.
Same account and cross-account are not the same mechanism
Because the trust policy is a resource-based policy, the union rule from the evaluation model applies to it. Within one account, AWS is explicit: when a resource-based policy grants access to a principal in the same account, no additional identity-based policy is required. A trust policy naming a user or role in the same account is sufficient on its own β the caller needs no sts:AssumeRole permission anywhere.
Across accounts it inverts. A user who wants to access a role in a different account must also have permissions that are delegated from the account administrator, so both sides must agree: the trust policy in the trusting account, and an identity policy in the trusted one. This is the single most useful property in the whole model, and it is the reason the vendor case is not as bad as it could be β the vendor's own administrator still has to grant it. That is also precisely why it is a delegation of trust to their IAM rather than to a product.
What each Principal form actually delegates
Four forms, four different sets.
An account
The account ARN and the shortened account ID behave the same way. Both delegate permissions to the account. And the detail that surprises almost everyone: using the account ARN in the Principal element does not limit permissions to only the root user of the account. Writing :root is not a restriction. It is the whole account.
A role ARN
Narrower, and it carries a trap. If your Principal element in a role trust policy contains an ARN that points to a specific IAM role, then that ARN transforms to the role's unique principal ID when you save the policy. The console renders it back as an ARN, so the transformation is invisible.
A service principal
Trusts the service, not the person who configured it. The service principal cannot be a wildcard Service value, which is a floor rather than a control. Note also that for IAM role trust policies, AWS recommends using the non-regionalized service principal name, because IAM resources are global.
A federated provider
Trusts every token that provider will issue. For a shared provider such as a CI platform, the provider ARN alone is close to meaningless as a restriction β the narrowing has to come from a condition on the token's own claims.
The recreate trap, stated plainly
Because a role ARN is stored as a principal ID, the failure mode is delayed and quiet: if you delete the role, then you break the relationship. The policy no longer applies, even if you recreate the role because the new role has a new principal ID that does not match the ID stored in the trust policy.
In an estate where roles are managed by Terraform, a resource replacement β a name change, a forced replacement, a destroy and re-apply in a lower environment β does exactly this. Nothing errors at apply time. The trust policy still renders the same ARN. The failure appears later as an AccessDenied on assumption, and the policy that is at fault is the one that looks correct.
Wildcards do not do what you might reach for
Two rules, and both are absolute. When you create a trust policy, you cannot specify a wildcard as part of an ARN in the principal element, and more generally you cannot use a wildcard to match part of a principal name or ARN. There is no arn:aws:iam::111122223333:role/app-*. The choices are an exact principal, an account, or "*" with conditions doing the work β and the third is public access unless the condition block is airtight.
Why This Architecture Holds Up
It puts the authentication decision in one reviewable document
Everything about who may become a role is in one JSON document attached to that role. There is no equivalent of a group membership list elsewhere, no directory to cross-reference, no inherited setting. That is a genuinely good property, and it is why "read every trust policy in the account" is a finite, worthwhile exercise that most estates have never performed.
The conditions are evaluated at assumption, not at use
A condition in a trust policy gates the minting of credentials. Once a session exists, the trust policy is out of the picture β which is why DurationSeconds matters as a security parameter and not just a convenience. It can range from 900 seconds up to the maximum session duration set for the role, and the maximum session duration setting can have a value from 1 hour to 12 hours. A twelve-hour session is twelve hours during which a tightened trust policy has no effect on the credentials already issued.
Role chaining puts a hard ceiling in the right place
Role chaining limits your AWS Management Console, AWS CLI or AWS API role session to a maximum of one hour, regardless of the maximum session duration configured for the individual roles. Chained access β the pattern most likely to be reached through an automation path nobody has fully mapped β is the pattern with the shortest credential life, which is the correct default even though it is usually encountered as an annoyance.
It composes with the organisation instead of duplicating it
For access within your own estate there is a condition key that removes the maintenance problem entirely. aws:PrincipalOrgID provides an alternative to listing all the account IDs for all AWS accounts in an organization, and when you add and remove accounts, policies that include the key automatically include the correct accounts and don't require manual updating. A trust policy written against the organisation does not decay as the organisation grows.
Key Architecture Decisions
The external ID exists to solve the confused deputy problem: the vendor holds your role ARN, another customer supplies the same ARN, and without something tying the call to you, the vendor can be tricked into acting on your account for someone else. AWS is direct about ownership β the ExternalId value must be unique among a provider's customers and controlled by the provider, not its customers. If the vendor invites you to invent your own, that is a signal about how they have built their multi-tenancy.
AWS does not treat the external ID as a secret. The external ID for a role can be seen by anyone with permission to view the role. It is an assertion of circumstance, not a credential β so do not design as though leaking it matters, and do not use it in place of a condition that actually restricts. The value must have a minimum of 2 characters and a maximum of 1,224 characters.
A service principal is an identifier for a service, and the service acts on behalf of whoever configured it. A bucket policy or trust policy naming cloudtrail.amazonaws.com with no condition will accept a trail configured in any account, by anyone who knows the resource name. AWS recommends using aws:SourceArn, aws:SourceAccount, aws:SourceOrgID, or aws:SourceOrgPaths in your resource policies wherever an AWS service principal is granted permission to access one of your resources.
Use aws:SourceArn when one specific resource is the caller, aws:SourceAccount when it is any resource in a known account, and aws:SourceOrgID when it is any account in your organisation. Applied at the RCP level, the last of these covers every service integration at once rather than one resource policy at a time.
Naming an OIDC provider ARN in the trust policy establishes that AWS will verify tokens from that issuer. It does not establish whose tokens. On a shared platform, the audience and subject claims are the only thing distinguishing your pipeline from every other tenant of the same issuer, and they belong in the condition block as an exact match wherever possible.
The trapA StringLike condition on a subject claim with a trailing wildcard is where this goes wrong β repo:my-org/* looks scoped and admits any repository whose subject happens to share that prefix pattern, including branches and environments you did not intend. Prefer StringEquals on the full claim, and add entries deliberately.
Role trust policy length has a default quota of 2048 characters and a maximum quota of 8192 characters β the ceiling is exactly four times the default, and the increase is one of the quota requests AWS auto-approves. That sounds generous until a trust policy enumerates thirty account IDs with a condition block on each.
ImplicationAn enumerated trust policy is not just a size problem, it is a correctness problem: the list is a copy of an org structure that changes without telling it. Use aws:PrincipalOrgID for your own accounts, and reserve explicit enumeration for principals genuinely outside your organisation β which is a short list by definition.
Which Principal, and what to add
| Principal | What it delegates | Add this condition | Fails when |
|---|---|---|---|
| Account ID or account ARN | Every principal in that account, present and future | sts:ExternalId for a third party; aws:PrincipalOrgID or aws:PrincipalArn internally |
Never β but the set silently grows with their org |
| Role or user ARN | That one principal, stored as a principal ID | MFA or source identity, if the caller is human | The principal is deleted and recreated |
| Service principal | The service, acting for whoever configured it | aws:SourceArn, aws:SourceAccount or aws:SourceOrgID |
A cross-Region call from an opt-in Region needs the regionalized name |
| OIDC provider | Every token that issuer will mint | Exact match on the audience and subject claims | A wildcard in the subject match widens it back out |
"*" |
Everyone, including anonymous callers | Everything the policy is relying on, with no gaps | Any gap at all β this is public by default |
Reference: a third-party trust policy that says what it means
The vendor case from the top of this post, written properly. The Principal is unchanged β it has to be the account, because the vendor's principals are theirs to create β and every narrowing is in the condition block.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VendorAssumesWithTheirOwnExternalId",
"Effect": "Allow",
"Principal": { "AWS": "222233334444" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "8f2c1d90-vendor-issued-per-customer"
},
"ArnLike": {
"aws:PrincipalArn": "arn:aws:iam::222233334444:role/CostCollector*"
}
}
}
]
}
Two things are happening. The external ID stops the vendor being confused into acting for another customer. The aws:PrincipalArn condition narrows the delegation from their whole account to a named role family β and it belongs in a condition rather than in Principal precisely because condition-key ARNs are not converted to principal IDs, so the vendor can rotate that role without breaking your policy. It is the one place where the recreate trap works in your favour.
Closing Thought
The permissions policy gets the attention because it is the one that fails loudly. Get it wrong and someone cannot do their job by Tuesday. Get the trust policy wrong and nothing happens at all, for a long time, until it does.
What makes it worth a deliberate review pass is that the mistakes are not subtle once you are looking for them. An account ID with no condition. A service principal with no source. A provider ARN with no claim check. A wildcard subject that was meant to save typing. Each is visible in a two-line read of a document that already exists, and there are usually fewer trust policies in an account than people expect.
The question to ask of every trust policy you own: if the other side reorganised tomorrow and told nobody, would this document still describe who I meant? An account ID does not survive that question. An organisation ID does. A vendor's external ID does. A specific role ARN survives it only until someone runs a destroy and re-apply.
Security & Identity — IAM Access Analyzer: turning the review this post describes into something a machine does continuously, and the difference between external access findings and unused access findings.
Official AWS Reference
- AWS Documentation β IAM roles: terms and concepts
- AWS Documentation β AWS JSON policy elements: Principal
- AWS Documentation β AWS STS AssumeRole API reference
- AWS Documentation β The confused deputy problem
- AWS Documentation β Access to AWS accounts owned by third parties
- AWS Documentation β AWS global condition context keys
- AWS Documentation β IAM and AWS STS quotas
Comments