Cloud IAM attack paths in 60 seconds
Cloud IAM decides which identities may perform which actions on which resources. A privilege escalation path appears when several individually reasonable grants compose into authority nobody intended to give to one principal. Reviewing policies one at a time can therefore miss the actual attack path.
The cloud security review that comes back clean is the one that worries me. Not because I assume the environment is broken, but because of what a clean scan actually measured. A CSPM scores each IAM policy against a rule set, one policy at a time. Privilege escalation is not a policy. It is a path through several of them, and most of those policies are individually defensible.
The scanner reads permissions. The attack is made of the gaps between permissions.
Identity versus resource: the inversion nobody reads for
IAM has two ways to say “this principal may do this thing.” The one everyone reads is the identity policy — the document attached to the user or role that lists what it may do. The one everyone misses is the resource policy — the document attached to the bucket, queue, or role that lists who may touch it.
They do not just look different. They point in opposite directions.
An identity policy says “this role can assume that role.” A resource policy — the trust policy on the target role — says “that principal may assume me.” Access is the intersection of the two, and one of them is written by the team that owns the resource, not the team that owns the principal. That split is where the escalation lives, because a CSPM that grades the principal’s policy will never open the resource’s policy to ask the other half of the question.
It is the same shape as the delegation note: the permission is stored on the target, and the target decides. In a directory you find it by asking who can write to an object. In a cloud account you find it by asking who is named in the trust policies, the resource policies, and the KMS key policies you never inventoried.
The chains that survive a policy scan
A scanner flags a policy that says * on *. Almost nothing in a real account says that.
The dangerous grants are narrow and boring, and they only become interesting when you read
them in sequence.
iam:CreatePolicyVersion. The principal can publish a new version of a customer-managed
policy. If that policy is attached to a higher role, the principal can rewrite what the
higher role is allowed to do — without touching the role, its trust policy, or anything the
scanner is looking at. The versioning feature exists precisely so policies can change, so a
grant that lets you use it looks like routine administration.
iam:UpdateAssumeRolePolicy. Write access to the trust policy means control over who
becomes the role. It does not need PassRole. The principal rewrites the trust document
to name itself, assumes the role, and is now whatever the role is. The role’s own policy was
never modified, so the finding — if any — lands on a principal nobody was watching.
sts:AssumeRole over a cross-account trust. The single most reliable pivot in the
cloud, and the least likely to be flagged, because the trust was created on purpose. Account
A’s role trusts a principal in account B. Compromise B and the path into A is already paved,
documented, and approved. The scanner cannot call it wrong; it was designed that way.
iam:PassRole read backwards. PassRole is the one everyone knows, and because everyone
knows it, the rule usually exists. The version that survives is indirect: a principal that
can attach a policy to a role it can then pass to a service. Neither grant is dangerous in
isolation. Together they are a role with an attacker-written policy, and the two statements
are never in the same policy document, so no single-policy check sees both.
Why the scanner cannot see it
A CSPM is a snapshot tool. It opens one policy, applies one rule, and emits one verdict. An escalation chain is a graph problem — the same argument as in the note on edge shelf life, but the edges here are trust relationships instead of ACLs, and the graph spans accounts instead of one directory.
A principal mapper closes exactly that gap. It builds the graph — who can assume what, who is named in which trust policy, what each role can attach and pass — and then walks it the way BloodHound walks a domain:
# Build the account's principal graph from the current credentials.
$ pmapper graph create --config config.json
# The question a CSPM never asks: what can this specific principal actually reach?
$ pmapper query "who can do s3:GetObject with admin"
$ pmapper query "preset privesc *"
The privesc preset is the useful half. It does not list over-permissive policies. It lists
chains — the sequence of grants that take a principal from where it is to where it should
not be. Two statements in different documents, linked by an AssumeRole, is exactly the kind
of thing that never appears in a policy-scoring report and is one query away in a graph.
Testing it as a path, not a snapshot
The productive cloud test does not start by scanning for *. It starts from a specific
principal — the one an attacker would plausibly land on — and asks what that principal can
become, step by step, using only durable grants.
# Enumerate every trust policy in the account — the resource-side half most scans skip.
$ aws iam list-roles --query 'Roles[].{Role:RoleName,Trust:AssumeRolePolicyDocument}'
# Then the cross-account surface, which lives outside any single account's scan.
$ aws organizations list-accounts --query 'Accounts[].Id'
The cross-account question is the one a single-account CSPM is structurally unable to answer. A trust from account A into account B is stored in A, but the risk it creates lives in B — and no tool scoped to B will ever flag a policy that lives in A. Someone has to ask about the trust graph directly, across the whole organisation, or the most reliable pivot in the environment stays invisible by construction.
Evidence matrix
| Signal | What it proves | Negative control | Defender verification |
|---|---|---|---|
| Identity grant and target trust/resource policy both permit the hop | The principal can cross the boundary; neither document proves that alone | Remove one side in a test role and confirm the assumption is denied | Use policy simulation plus Access Analyzer against both sides of the relationship |
A scoped benign action succeeds after AssumeRole | The path is executable from the starting principal | Add a boundary or condition that should deny the same action and repeat | Correlate STS and downstream API events in CloudTrail |
PassRole and service creation/update rights resolve to the same role | A service-mediated privilege path exists across separate statements | Restrict iam:PassedToService or the role resource and confirm the chain breaks | Query principals that can both pass the role and invoke a consuming service |
| Cross-account hop starts from an account outside the target scan | The escalation graph spans administrative ownership boundaries | Revoke or condition the external trust and repeat from the same source | Build the trust graph at organisation scope, including external IDs and principal conditions |
The pattern I keep seeing
Every dangerous grant in these chains was made for a reason that was correct at the time.
A deployment role needed to update a policy to roll out a change, so it got
CreatePolicyVersion. A vendor needed to assume a role, so a cross-account trust was
created. A service needed a role, so someone wired up PassRole without noticing that the
same principal could also attach a policy first.
None of them look wrong. They are ordinary administration, and that is exactly why a rule-based scanner will not surface them: there is no misconfiguration to point at, only a sequence of decisions nobody re-read in order.
The same dynamic that keeps a certificate authority unreviewed because it sits between two teams keeps cloud trust unreviewed because it sits between two accounts. The identity team owns the principal, the platform team owns the role, and neither owns the path.
What to hand the defenders
The path, not the policy. Name the specific principal, the sequence of grants it can walk, and the single earliest one whose removal breaks the chain. A report that says “IAM is over-permissive” produces a cleanup project. A report that says “this service role can publish a policy version, assume the admin role, and cross into production” produces a ticket.
Separate identity from resource. Every trust policy, resource policy, and KMS key policy is an access decision that lives on the wrong side of the scan. Inventory them explicitly, because the identity-side tooling will never do it for you.
Graph the trust across accounts. The organisation-wide principal map is the only deliverable that captures cross-account pivoting, and it is the one most organisations have never built. Build it once and re-run it on a schedule, because trust accretes the way permissions always do — a new vendor, a new account, a new “temporary” grant.
The scanner is not wrong. It is answering a different question than the one the attacker is asking.
How current is this note?
The latest source-review, content-update, or publication date is shown.
The author completed a technical review. This does not, by itself, claim lab reproduction.
