Home Resume
Homeβ€Ί Blogβ€Ί AWS Architecture Series #37 β€” The other half of the role…
AWS Architecture AWS Architecture Series

AWS Architecture Series #37 β€” The other half of the role

A vendor is onboarded with a role whose trust policy names their AWS account and nothing else. Everyone involved believes access was granted to the vendor. What was granted was the ability of that vendor's IAM administrator to hand the role to anyone, at any point in the future, with no signal on this side of the boundary.

Verified against current vendor documentation on 30 August 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

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.

Three different mistakes, one shape

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.

Diagram: a role's two policies, the permissions policy and the trust policy; four forms of the Principal element and what each actually delegates β€” an account, a role ARN stored as a principal ID, a service principal, a federated provider; and the condition keys that narrow each case
The Principal element names a set. The Condition block is what makes that set smaller than the whole account, the whole service, or the whole provider.

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

1
For a third party, require an external ID β€” and take it from them

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.

The misconception to drop

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.

2
Never trust a service principal without a source condition

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.

Concrete rule

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.

3
For OIDC federation, the provider is the plumbing and the claim is the control

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 trap

A 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.

4
Budget the document, and use the organisation rather than a list

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.

Implication

An 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.

Trust policy — third-party vendor role
{
  "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.

Next in this series

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.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent