Find Cloud IAM Privilege Escalation Paths Before Attackers Do
This is for developers and platform engineers who need a repeatable way to detect IAM privilege escalation paths in cloud environments. You’ll end with a graph-backed list of risky principals, the exact permissions enabling escalation, and CLI checks you can rerun in CI or on a schedule.
TL;DR — You are looking for principals that can grant themselves broader access indirectly: attaching policies, passing roles, updating trust policies, creating access keys, impersonating service accounts, or changing federation bindings. The fastest reliable workflow is: export IAM state, run a privilege-escalation graph tool, then confirm each finding with cloud-native CLI policy simulation before you remediate. Reading time: ~5 min
Goal
When you finish, you will have a current, reproducible report of cloud IAM privilege escalation paths in your environment, plus CLI-verified evidence for each path so you can remove or constrain the exact permission causing it.
Prerequisites
- Read access to IAM in at least one cloud account/project/subscription
- Permission to list identities, roles, policies, and trust bindings in your environment
- One of these installed locally:
- AWS CLI v2 — check with
aws --version - Azure CLI — check with
az version - gcloud CLI — check with
gcloud version
- AWS CLI v2 — check with
- Python 3.10+ — check with
python3 --version jq1.6+ — check withjq --version- Git — check with
git --version - A writable working directory for exported IAM data
- The account IDs, subscription IDs, or project IDs you want to assess
- Optional but recommended: a disposable test principal you can use for policy simulation without touching production identities
Steps
Step 1: Create a working directory and verify your active cloud context
mkdir -p ~/iam-escalation-review && cd ~/iam-escalation-review
aws sts get-caller-identity 2>/dev/null || true
az account show 2>/dev/null || true
gcloud config list --format='text(core.account,core.project)' 2>/dev/null || true
You should see your current account/subscription/project identity; if a command is not relevant, it will print nothing and continue.
Step 2: Export IAM state from the cloud you are assessing
For AWS:
mkdir -p aws
aws iam list-users > aws/users.json
aws iam list-roles > aws/roles.json
aws iam list-policies --scope Local > aws/customer-managed-policies.json
aws iam get-account-authorization-details > aws/account-authorization-details.json
You should get JSON files under aws/ with no AccessDenied errors.
For Azure:
mkdir -p azure
az role assignment list --all > azure/role-assignments.json
az role definition list > azure/role-definitions.json
az ad user list --all > azure/users.json
az ad group list --all > azure/groups.json
az ad sp list --all > azure/service-principals.json
You should get JSON files under azure/; large tenants may take a minute.
For Google Cloud:
mkdir -p gcp
PROJECT_ID="your-project-id"
gcloud projects get-iam-policy "$PROJECT_ID" --format=json > gcp/project-iam-policy.json
gcloud iam roles list --project "$PROJECT_ID" --format=json > gcp/project-roles.json
gcloud iam service-accounts list --project "$PROJECT_ID" --format=json > gcp/service-accounts.json
You should get JSON files under gcp/ with bindings and role definitions.
Step 3: Run a privilege-escalation path analyzer
Use an open-source graph tool that models escalation edges. For AWS, a common choice is Pacu’s privesc module or a dedicated IAM graph analyzer; for Azure and GCP, BloodHound-compatible collectors are common. The portable pattern is: collect JSON, then analyze relationships offline.
For AWS with Pacu in a Python virtual environment:
python3 -m venv .venv
. .venv/bin/activate
pip install pacu
pacu --help
You should see Pacu usage output, not command not found.
For Azure/GCP with BloodHound-compatible collection, install the collector you use in your environment and produce JSON/ZIP output, then load it into your graph tool. If you already have collector output, skip collection and go straight to analysis.
A minimal AWS-native fallback if you do not use a graph tool yet is to grep for known escalation permissions in exported policy documents:
jq -r '.. | .Action? // empty | arrays[]?, strings? ' aws/account-authorization-details.json | sort -u | egrep 'iam:PassRole|iam:Attach(User|Group|Role)Policy|iam:Put(User|Role)Policy|iam:UpdateAssumeRolePolicy|iam:CreateAccessKey|sts:AssumeRole|lambda:CreateFunction|glue:CreateDevEndpoint|cloudformation:CreateStack'
You should see a shortlist of high-risk actions if they exist in any attached or inline policy.
Step 4: Confirm each finding with cloud-native CLI checks
For AWS, inspect the principal and simulate the suspicious action. Replace the ARN values with your finding.
PRINCIPAL_ARN="arn:aws:iam::123456789012:user/dev-user"
POLICY_SOURCE_ARN="$PRINCIPAL_ARN"
TARGET_ROLE_ARN="arn:aws:iam::123456789012:role/AdminRole"
aws iam simulate-principal-policy \
--policy-source-arn "$POLICY_SOURCE_ARN" \
--action-names iam:PassRole sts:AssumeRole iam:AttachUserPolicy iam:UpdateAssumeRolePolicy \
--resource-arns "$TARGET_ROLE_ARN"
You should see EvalDecision values such as allowed or explicitDeny; allowed on any escalation action is a real lead.
Typical successful output shape:
{
"EvaluationResults": [
{
"EvalActionName": "iam:PassRole",
"EvalResourceName": "arn:aws:iam::123456789012:role/AdminRole",
"EvalDecision": "allowed"
}
]
}
For Azure, inspect effective role assignments for the principal and look for role-management or managed-identity control permissions:
ASSIGNEE_OBJECT_ID="00000000-1111-2222-3333-444444444444"
az role assignment list --assignee "$ASSIGNEE_OBJECT_ID" --all -o table
You should see assigned roles and scopes; Owner, User Access Administrator, or custom roles containing Microsoft.Authorization/roleAssignments/write are escalation-capable.
For GCP, inspect bindings and role contents for impersonation or IAM mutation permissions:
gcloud projects get-iam-policy "$PROJECT_ID" \
--flatten="bindings[].members" \
--format="table(bindings.role,bindings.members)"
gcloud iam roles describe roles/iam.serviceAccountTokenCreator --format=json
You should see whether a principal can impersonate service accounts or change IAM policy bindings.
Step 5: Record the exact escalation edge and remove the smallest permission set
Create a CSV of findings with principal, action, target, and fix owner.
cat > findings.csv <<'csv'
principal,cloud,action,target,evidence,owner
arn:aws:iam::123456789012:user/dev-user,aws,iam:PassRole,arn:aws:iam::123456789012:role/AdminRole,simulate-principal-policy=allowed,platform
csv
column -s, -t findings.csv
You should see a readable table you can hand to the role owner.
Use this decision rule when remediating:
- Remove
iam:PassRoleunless the resource ARN is tightly scoped and paired with a condition on the exact service. - Remove
iam:UpdateAssumeRolePolicy,iam:Attach*Policy,iam:Put*Policy, andiam:CreateAccessKeyfrom human principals unless there is a break-glass process. - In Azure, remove
Owner,User Access Administrator, or custom roles withroleAssignments/writefrom broad scopes. - In GCP, remove
resourcemanager.projects.setIamPolicy,iam.serviceAccounts.actAs, andiam.serviceAccountTokenCreatorwhere not strictly required.
⚠️ Removing IAM permissions can break deployments, CI jobs, and runtime service access. Test changes first on a non-production principal or at the narrowest scope, then re-run the verification commands before broad rollout.
Step 6: Re-run the analyzer and keep the check on a schedule
Re-run your export and simulation after changes.
aws iam get-account-authorization-details > aws/account-authorization-details.json
jq -r '.. | .Action? // empty | arrays[]?, strings? ' aws/account-authorization-details.json | sort -u | egrep 'iam:PassRole|iam:Attach(User|Group|Role)Policy|iam:Put(User|Role)Policy|iam:UpdateAssumeRolePolicy|iam:CreateAccessKey|sts:AssumeRole' || true
You should see fewer or no high-risk actions for the remediated principals.
Verify it works
Run the same cloud-native checks against a principal you remediated.
For AWS:
aws iam simulate-principal-policy \
--policy-source-arn "arn:aws:iam::123456789012:user/dev-user" \
--action-names iam:PassRole iam:AttachUserPolicy iam:UpdateAssumeRolePolicy \
--resource-arns "arn:aws:iam::123456789012:role/AdminRole"
Expected result: EvalDecision is implicitDeny or explicitDeny for the escalation actions you removed.
For Azure:
az role assignment list --assignee "00000000-1111-2222-3333-444444444444" --all -o json | jq '.[].roleDefinitionName'
Expected result: the principal no longer has Owner, User Access Administrator, or the risky custom role at the broad scope.
For GCP:
gcloud projects get-iam-policy "$PROJECT_ID" --format=json | jq '.bindings[] | select(.role|test("owner|iam.securityAdmin|iam.serviceAccountTokenCreator"; "i"))'
Expected result: the remediated principal is absent from risky bindings, or the binding scope is reduced to the intended resource.
Common pitfalls
Step output shows AccessDenied or AuthorizationFailed
Mistake: running exports with a principal that can deploy apps but cannot read IAM metadata. Symptom: AWS returns An error occurred (AccessDenied) when calling the GetAccountAuthorizationDetails operation; Azure returns AuthorizationFailed; GCP returns PERMISSION_DENIED. Fix: switch to a read-only security/audit role that includes IAM read permissions, then rerun the export.
Graph tool reports a path, but CLI simulation says implicitDeny
Mistake: analyzing stale collector output after a recent policy change. Symptom: the graph still shows an escalation edge, but simulate-principal-policy or current role assignment checks deny it. Fix: re-collect IAM data and rebuild the graph before opening remediation work.
You only searched for AdministratorAccess
Mistake: looking for obvious admin roles and missing indirect escalation permissions. Symptom: no findings, but users still have iam:PassRole, policy attachment rights, or service-account impersonation. Fix: search for the exact mutation and impersonation actions listed in Step 5, not just admin role names.
iam:PassRole looks harmless because sts:AssumeRole is denied
Mistake: treating PassRole as safe when a service can assume the role on the user’s behalf. Symptom: a user cannot directly assume AdminRole but can create or update a compute resource that runs with it. Fix: remove iam:PassRole or scope it to specific role ARNs plus service conditions.
Azure or GCP custom roles hide the real risk
Mistake: checking role names only. Symptom: the role is named something benign like deployment-operator, but it contains Microsoft.Authorization/roleAssignments/write or resourcemanager.projects.setIamPolicy. Fix: inspect the role definition JSON and search for permission strings, not display names.
Remediation breaks CI/CD immediately
Mistake: removing a broad permission without first identifying the exact resource or service dependency. Symptom: deployment jobs start failing with authorization errors right after the IAM change. Fix: re-add the permission at the narrowest scope needed, then replace it with a constrained resource ARN, condition, or service-specific role.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI