Most cloud breaches do not start with a root key. They start with a small identity that has one permission too many, and that permission opens a path to a bigger one. IAM privilege escalation is the name for that path: a low privilege user or role in a cloud account walks a chain of allowed actions until it reaches admin. This post explains the classic shapes of that walk, shows a short example of each, and gives the defenses that close them.
What IAM privilege escalation actually is
IAM is the system that decides who can do what in a cloud account. Every identity carries policies, and every policy is a list of allowed or denied actions on resources. An attacker who lands on a low privilege identity does not need to break the policy engine. They just need to use the permissions they already have to grant themselves more.
Think of it as a graph. Each identity is a node. Each allowed action that changes another identity or grants a new permission is an edge. Escalation exists when there is a path from where you start to a node that has admin. You do not need one giant mistake. You need a sequence of small ones that connect.
Escalation is not a single bad permission. It is a path of allowed actions that leads from a small identity to a big one.
The classic shapes of IAM privilege escalation
Almost every real world case is one of a few patterns. Here they are, with a short example each. These are educational and defensive. They describe how an over permissive policy allows escalation so you can find and remove it in your own account, using invented names only.
1. A principal that can edit its own policies
Say a role named ci-runner is allowed to attach policies to itself. On the surface that sounds like a convenience for a build pipeline. In practice it means the role can attach the managed admin policy and grant itself everything.
{
"Effect": "Allow",
"Action": "iam:AttachRolePolicy",
"Resource": "arn:aws:iam::123456789012:role/ci-runner"
}
With that single statement, ci-runner can run iam:AttachRolePolicy and point it at AdministratorAccess. One allowed action, one edge in the graph, and the role is now admin. The same problem appears with iam:PutRolePolicy, which lets an identity write an inline policy on itself with no size or content limit.
2. Permission to pass a strong role to a service
The action iam:PassRole lets an identity hand an existing role to a service such as Lambda, EC2, or Glue. That is normal and needed. It turns dangerous when the identity can pass a role that is far stronger than the identity itself, and can also start the service that will run under it.
{
"Effect": "Allow",
"Action": ["iam:PassRole", "lambda:CreateFunction", "lambda:InvokeFunction"],
"Resource": "*"
}
Here a low privilege user can create a function, pass the admin role to it, and invoke it. The code inside the function then runs with admin rights. The user never gained admin directly. They borrowed it through a service they were allowed to launch. This pass role step is one of the most common edges in a real escalation path.
3. Permission to mint new credentials or a new policy version
Two quieter actions do the same job. The first is iam:CreateAccessKey on another user. If your identity can create an access key for an admin user, you can create a key, sign in as that admin, and skip the rest.
{
"Effect": "Allow",
"Action": "iam:CreateAccessKey",
"Resource": "arn:aws:iam::123456789012:user/*"
}
The second is iam:CreatePolicyVersion. A managed policy keeps a history of versions, and you can set a new version as the default. If your identity can create a new version of a policy that is attached to you, you can write a version that allows every action and mark it default. The old restrictive version stays in history while the new one takes effect. Both actions look administrative and boring, and both hand over full control.
4. Wildcards in actions or resources
Wildcards are where most of these paths hide. A policy that grants "Action": "iam:*" on "Resource": "*" contains every escalation above at once. Even a narrower wildcard like iam:Create* quietly includes iam:CreateAccessKey and iam:CreatePolicyVersion.
{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
}
People write wildcards because listing exact actions is tedious and because a broad grant makes the error message go away during setup. The wildcard then sits there for years. When you audit a policy, a wildcard in the action or the resource field is the first thing to read closely, because it may be granting far more than anyone intended.
Why a graph reasoner is the right tool here
Notice the common thread. None of these findings is visible from one policy read in isolation. The iam:PassRole grant is only dangerous when paired with permission to start a service. The iam:CreatePolicyVersion grant only matters when the policy in question is attached to you. Escalation is a property of how permissions connect, not of any single line.
That makes it a reachability problem. Build the graph: identities as nodes, permission granting actions as edges. Then ask a plain question. Is there a path from this low privilege identity to any node that holds admin? If yes, that path is the escalation, and every edge on it is a permission you can remove. This is exactly the kind of question a graph reasoner answers well, because it follows the path across many policies instead of judging each policy alone. It is the same style of reasoning you would use to trace whether an exposed bucket in an S3 bucket misconfiguration connects to something that matters, or whether a leaked credential from the instance metadata service reaches an admin role.
How to prevent IAM privilege escalation
The defenses map directly onto the shapes above. None of them is exotic. They are about removing edges from the graph so no path to admin remains.
- Grant least privilege. Give each identity only the actions it needs for its job, scoped to the exact resources it touches. A build role needs to deploy, not to rewrite IAM.
- Do not let identities manage their own policies. Deny
iam:AttachRolePolicy,iam:PutRolePolicy, andiam:CreatePolicyVersionwhere the target is the identity itself. Self editing is the shortest path to admin. - Restrict pass role. Scope
iam:PassRoleto the specific roles a service may assume, and use a condition on the service that is allowed to receive it. Never pair a wide pass role grant with permission to launch compute. - Deny wildcards in sensitive actions. Avoid
iam:*and*resources on any identity that does not need them. Prefer an explicit action list, and add an org wide deny for the handful of actions that create credentials or policy versions. - Review the permission graph, not single policies. Read your account as a graph and look for any path from a normal identity to admin. Access analyzer style tooling and regular reviews catch edges a per policy read misses.
Here is the whole idea in one concrete run. An account has a role called reporting meant only to read billing data. Someone added iam:CreateAccessKey on all users so a script could rotate keys. Read alone, that grant looks like housekeeping. Read as a graph, it is an edge from reporting to every admin user in the account, which means reporting is effectively admin. Remove that one edge and the path is gone. That is the shape of every fix here: find the edge, cut it, recheck the path.
Privilege escalation in the cloud is a permission graph problem before it is anything else, and the same logic applies on a single host, which we cover in what is privilege escalation, and across our other deep dives. UnboundCompute reasons about paths through a system, so it treats questions like this as reachability over how a system connects rather than a checklist of single rules. You can read more about that approach on our about page.
Frequently asked questions
What is IAM privilege escalation?
IAM privilege escalation is when a low privilege user or role in a cloud account uses the permissions it already has to grant itself more, until it reaches admin. It is a path of allowed actions, not a single broken rule. Each identity is a node and each permission granting action is an edge, and escalation exists when a path connects a small identity to a big one.
How does the iam:PassRole permission lead to escalation?
The iam:PassRole action lets an identity hand an existing role to a service such as Lambda or EC2. It becomes dangerous when a low privilege identity can pass a role that is much stronger than itself and can also start the service that runs under it. The user creates a function, passes the admin role to it, invokes it, and the code runs with admin rights the user never held directly.
Why are wildcards in IAM policies risky?
A wildcard like iam:* on Resource: * grants every escalation action at once, and even a narrower iam:Create* quietly includes iam:CreateAccessKey and iam:CreatePolicyVersion. People add wildcards to make setup errors go away, then the broad grant sits unused for years. When auditing a policy, read any wildcard in the action or resource field closely.
How do you prevent IAM privilege escalation?
Grant least privilege scoped to exact resources, do not let identities edit their own policies, restrict iam:PassRole to specific roles and services, deny wildcards on sensitive actions, and review your account as a permission graph rather than one policy at a time. The goal is to remove edges so no path from a normal identity to admin remains.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
