In AWS a principal does not need an administrator policy to become an administrator: it only needs one permission that lets it change its own access, create credentials for someone stronger, or hand a stronger role to a service it controls. These privilege escalation paths are rarely visible in a policy review, because each permission looks reasonable on its own. This guide is a defender's runbook for finding them across an account or an AWS Organization: export the IAM data, test every principal for the high-risk permission classes with the policy simulator and IAM Access Analyzer, follow role trust chains, then rank and fix what you find. It is for cloud security engineers and IAM reviewers. The policy patterns behind the fixes are covered in the AWS IAM Policies and Roles Guide and the Top 10 AWS IAM Mistakes; this page is about the hunt.
This is an independent guide from NHI Mgmt Group, based on AWS's public documentation as of November 2026. It is not sponsored or reviewed by AWS. Product features change, so confirm settings against AWS's current documentation.
Key takeaways
- Hunt by permission class, not by policy name. Four classes cover most paths: IAM policy edits, credential creation for other principals, trust policy edits, and passing roles to compute.
- Test effective access, not text.
aws iam simulate-principal-policyevaluates identity policies, the attached permissions boundary and SCPs, so a permission blocked by a guardrail drops out of the results. - PassRole leaves no event of its own. AWS states that PassRole is a permission, not an API call, so the evidence sits in the
CreateFunction,RunInstancesor similar call that received the role. - Follow the chain. A modest role that can assume, or pass, a stronger role inherits that role's reach. Score a path by where it ends, not where it starts.
- Block regressions in the pipeline. IAM Access Analyzer
check-access-not-grantedfails a policy that grants any action on your sensitive list before it is deployed.
The permission classes to look for
The table lists the families of permissions that let a principal raise its own privilege. It is a detection list, not a procedure: each row is something a reviewer should treat as administrative.
| Class | Example actions to test | Why it escalates | Where the evidence appears in CloudTrail |
|---|---|---|---|
| Policy edits | iam:CreatePolicyVersion, iam:SetDefaultPolicyVersion, iam:AttachUserPolicy, iam:AttachRolePolicy, iam:PutUserPolicy, iam:PutRolePolicy, iam:AttachGroupPolicy, iam:PutGroupPolicy, iam:AddUserToGroup | The principal can widen its own or a stronger principal's permissions | The named IAM calls themselves |
| Credentials for others | iam:CreateAccessKey, iam:CreateLoginProfile, iam:UpdateLoginProfile | The principal can sign in or call APIs as a more privileged user | The named IAM calls, with a target user different from the caller |
| Trust edits | iam:UpdateAssumeRolePolicy | The principal can add itself to the trust policy of a stronger role | UpdateAssumeRolePolicy, then AssumeRole |
| Guardrail removal | iam:DeleteRolePermissionsBoundary, iam:DeleteUserPermissionsBoundary, iam:PutRolePermissionsBoundary | Removes the ceiling that made a delegated permission safe | The named IAM calls |
| Pass role to compute | iam:PassRole combined with create or update rights on a compute service, for example lambda:CreateFunction, ec2:RunInstances, cloudformation:CreateStack, glue:CreateDevEndpoint | Code the principal controls runs with the passed role's permissions | The create or update call, whose request parameters name the role ARN |
| Update existing compute | lambda:UpdateFunctionCode on a function with a privileged role; ssm:SendCommand on an instance with a privileged profile | The principal runs its own code under a role it was never given | The update or command call |
| Assume role | sts:AssumeRole on * or on privileged roles whose trust policy allows the caller | Direct hop to a stronger role | AssumeRole |
Extend the list for your estate. Any service that accepts a role ARN and runs customer code (container tasks, notebooks, build projects, data pipelines) belongs in the pass-role class.
Before you start
- Access: a read-only audit role in each account with IAM read permissions,
iam:SimulatePrincipalPolicyandaccess-analyzer:CheckAccessNotGranted. Simulation output shows other principals' effective permissions, so we recommend treating this role and its output as sensitive. - Scope: start with production accounts and the management account. Exclude service-linked roles, which are controlled by their owning service.
- Define "privileged": agree which roles are targets, for example any role with
AdministratorAccess, IAM write, or access to production data stores. A path matters because of where it ends. - Cost: AWS describes charges for IAM Access Analyzer custom policy checks; confirm current rates on the IAM Access Analyzer pricing page, and run checks on changed policies in the pipeline rather than on every policy every day.
Step-by-step
All steps are read only until step 7.
- Export the account's IAM data.
This returns users, groups, roles, their inline and attached policies with versions, and role trust policies in one file. Keep it as the baseline for the hunt and for later comparison.aws iam get-account-authorization-details --output json > iam-auth-ACCOUNT_ID.json - List candidate principals. From the export, take every user and role except service-linked roles. Mark which ones are non-human (roles assumed by services, CI, workloads); in most estates they far outnumber people and are reviewed less often.
- Simulate the high-risk actions for each principal.
The resource defaults toaws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME \ --action-names iam:CreatePolicyVersion iam:AttachRolePolicy iam:PutRolePolicy \ iam:CreateAccessKey iam:UpdateAssumeRolePolicy iam:PassRole \ iam:DeleteRolePermissionsBoundary lambda:CreateFunction lambda:UpdateFunctionCode \ --query "EvaluationResults[?EvalDecision=='allowed'].[EvalActionName,EvalResourceName]" \ --output text*, which answers "is this allowed anywhere". For a precise answer, rerun with--resource-arnsset to your privileged roles and users: a PassRole scoped torole/app-*is only a path if a privileged role matches the prefix. The simulator applies the entity's permissions boundary and SCPs, but it does not fetch resource-based policies, does not support resource control policies, and AWS warns that results can differ from live behaviour for role chaining and some advanced configurations. Treat anallowedas a finding to confirm, and animplicitDenyas reasonably reliable for identity policies. - Pair pass-role findings with compute rights. PassRole alone does nothing. A principal is on a pass-role path only if it can also create or update something that runs with the passed role, and the passed role is stronger than the caller. Record the pair (for example PassRole on
role/data-*pluslambda:CreateFunction) and the strongest role it can reach. - Follow trust chains. For each privileged role, read its trust policy from the export and list who can assume it: principals in the same account with
sts:AssumeRoleon it, other accounts, and federated identity providers. Then repeat for those principals. A role that trusts an account root lets any principal in that account with suitable STS permissions assume it. Stop at three hops for the first pass; most real paths are short. - Rank the paths. Score each by end point, number of hops and how the start principal authenticates (long-term key, federated CI, human SSO). Use the priority table below.
- Fix, starting with the cheapest break in each chain. Usually that is scoping the first hop: restrict PassRole by role ARN with
iam:PassedToService, remove IAM write actions from workload roles, replace root principals in trust policies with specific role ARNs, and require a permissions boundary with a deny on its removal for teams that create roles. AWS warns against controlling PassRole with resource tags because the results are not reliable. Make the change in infrastructure as code, deploy, and watch forAccessDeniedfrom the changed principal.
Prioritise what you find
| Path | Priority (NHI Mgmt Group view) | Typical fix |
|---|---|---|
| Principal with a long-term access key can reach an administrator role in one hop | 1 | Remove the IAM write or PassRole permission; move the workload to a role |
| CI or federated role can edit policies or trust on itself or others | 1 | Remove IAM write from CI; deploy IAM through a separate, reviewed pipeline role |
| Unscoped PassRole plus compute create rights | 2 | Scope PassRole to named role ARNs with iam:PassedToService |
| Role creation without a locked permissions boundary | 2 | Require the boundary on create and deny boundary deletion |
| Multi-hop chain ending in production data access | 3 | Break the weakest hop; document any chain you keep |
Make it continuous
- Pipeline gate: run
aws accessanalyzer check-access-not-granted --policy-type IDENTITY_POLICY --policy-document file://policy.json --access '[{"actions":["iam:PassRole","iam:CreatePolicyVersion","iam:AttachRolePolicy","iam:PutRolePolicy","iam:UpdateAssumeRolePolicy","iam:CreateAccessKey"]}]'on every changed workload policy. AFAILmeans the policy might allow at least one listed action; the check accepts up to 100 actions and 100 resources per access entry. Allow exceptions only for the IAM deployment role. - Diff the export: rerun step 1 weekly and compare with the baseline; new IAM write or PassRole grants go to the review queue.
- Detect use: alert on
CreatePolicyVersion,SetDefaultPolicyVersion,Attach*Policy,Put*Policy,UpdateAssumeRolePolicy,CreateAccessKeyandCreateLoginProfilewhere the target differs from the caller, and onDelete*PermissionsBoundary. For PassRole, alert onCreateFunction,RunInstancesandCreateStackevents whose request parameters reference privileged role ARNs. - Trim unused rights: IAM Access Analyzer unused access findings and last accessed data often show that the risky permission was never used. Removing it is then low risk. See the IAM Access Analyzer Guide.
Verify it worked
- Re-simulate: the same
simulate-principal-policycall for a fixed principal returnsimplicitDenyorexplicitDenyfor the removed actions, orallowedonly on non-privileged resource ARNs. - Re-walk the chain: the path's end point is no longer reachable from its start principal.
- Pipeline: a test policy containing
iam:PassRoleon*fails the gate. - No breakage: CloudTrail shows no new
AccessDeniederrors from the workload over a full release cycle.
Rollback and common errors
| Situation | Fix |
|---|---|
| Deployment fails after PassRole scoping | Read the role ARN in the error, add it to the allowed list or prefix, and check iam:PassedToService names the right service principal |
| Customer managed policy edit breaks a workload | Set the previous policy version as default; AWS keeps up to five versions |
| Simulator says allowed, live call is denied | A resource policy, RCP, VPC endpoint policy or session policy is denying it; record the guardrail as a compensating control |
| Simulator says denied, but CloudTrail shows success | The access came through a resource-based policy or a role chain the simulator does not evaluate; follow the trust path manually |
| Team cannot create roles after boundary enforcement | Confirm their create calls pass the boundary ARN; adjust the boundary, not the deny |
Small account or large organisation
A single account with tens of roles can be covered with the export, a short loop over simulate-principal-policy, and a spreadsheet of trust relationships. An organisation with hundreds of accounts needs the audit role deployed by StackSets, results collected centrally, and a graph of trust relationships across accounts, because many paths cross account boundaries. Purpose-built graph tooling helps at that scale; whatever you use, validate its findings with the simulator before raising a change.
Metrics
| Metric | Target direction |
|---|---|
| Principals able to reach an administrator role in one hop (excluding approved admin roles) | Zero |
Workload roles with unscoped iam:PassRole | Zero |
| Non-IAM-pipeline roles with any IAM write action | Falling to zero |
| Delegated roles carrying the required permissions boundary | 100% |
| Changed policies checked by the pipeline gate | 100% |
| Median days from new path found to fix | Falling |
Common failure patterns
- Grepping for "Action": "*": administrator policies are found, while
iam:PutRolePolicyon a CI role passes review. - Reviewing permissions without targets: PassRole on
*is reported, but nobody checks which privileged roles it can reach, so the fix is never prioritised. - Ignoring the second hop: each role is reviewed in isolation and a trust chain to production is missed.
- Boundary without a lock: a permissions boundary is required at creation, but nothing denies deleting it afterwards.
- Trusting the simulator as live truth: AWS states results can differ from live behaviour; confirm critical findings and fixes with CloudTrail.
Framework mapping
| Framework | Reference | How this task supports it |
|---|---|---|
| NIST SP 800-53 Rev 5 | AC-6 Least Privilege; AC-6(5) Privileged Accounts; AC-6(9) Log Use of Privileged Functions | Removes hidden administrative capability; logs its use |
| NIST CSF 2.0 | PR.AA-05 | Permissions reviewed with least privilege and separation of duties |
| MITRE ATT&CK | T1098.001 Additional Cloud Credentials; T1098.003 Additional Cloud Roles; T1548.005 Temporary Elevated Cloud Access; T1078.004 Cloud Accounts | Detection and prevention of the escalation behaviours in the class table |
Practitioner checklist
- Deploy a read-only audit role and treat its output as sensitive.
- Export IAM data with
get-account-authorization-detailsfor every in-scope account. - Define the privileged end points before hunting.
- Simulate the high-risk action list for every user and role, then rerun against privileged resource ARNs.
- Pair PassRole findings with compute create and update rights.
- Walk trust policies for privileged roles to three hops.
- Fix the cheapest break in each chain through infrastructure as code.
- Gate changed policies with
check-access-not-grantedand alert on IAM write events.
Standards and references
- AWS: Test IAM policies with the IAM policy simulator
- AWS CLI Reference: iam simulate-principal-policy
- AWS: Grant a user permissions to pass a role to an AWS service
- AWS: IAM Access Analyzer custom policy checks
- AWS CLI Reference: accessanalyzer check-access-not-granted
- MITRE ATT&CK: Additional Cloud Roles
- MITRE ATT&CK: Temporary Elevated Cloud Access
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: AWS IAM Policies and Roles Guide · Top 10 AWS IAM Mistakes · IAM Access Analyzer Guide · How to find unused AWS IAM roles and access keys · Cloud PAM and CIEM Guide