Skip to main content

Role Privilege Escalation Remediation

Triage and Remediation​

Remediation​

Using Console

Below is a practical, console-focused process to find and fix IAM role permissions that allow privilege escalation in AWS.


1. Understand What You’re Looking For​

Privilege escalation usually happens when a principal (user/role) can:

  • Attach/Update IAM policies to themselves or to roles they can assume:
    • iam:AttachUserPolicy, iam:AttachRolePolicy, iam:PutUserPolicy, iam:PutRolePolicy, iam:CreatePolicyVersion, iam:SetDefaultPolicyVersion, etc.
  • Assume or pass roles with more privileges:
    • sts:AssumeRole
    • iam:PassRole (combined with other actions)
  • Create or modify access keys / login profiles:
    • iam:CreateAccessKey, iam:UpdateAccessKey, iam:CreateLoginProfile, iam:UpdateLoginProfile
  • Create/Update Lambda, EC2, or other services with privileged roles:
    • lambda:UpdateFunctionCode, lambda:CreateFunction, ec2:RunInstances with a powerful instance profile, etc.

Your goal: Remove or tightly restrict these permissions to least privilege.


2. Inventory IAM Roles and Attached Policies​

  1. Sign in to the AWS Management Console with an admin account.
  2. Go to IAM:
    • Top right search bar → type “IAM” → IAM.
  3. In the left navigation, check:
    • Roles
    • Users
    • Policies

You’ll review permissions on:

  • Managed policies (AWS-managed & Customer-managed).
  • Inline policies on users/roles/groups.

3. Identify High-Risk Policies Using IAM Access Analyzer​

  1. In IAM console, left menu → Access Analyzer → Policy analyzer.
  2. Use Policy analyzer to search for policies containing sensitive actions:
    • In Actions filter, enter:
      • iam:*, sts:*, lambda:*, ec2:RunInstances, etc.
  3. For each policy result, open it in a new tab and inspect:
    • Look for statements with Effect: Allow and:
      • Action: "iam:*" or broad wildcards like "*" on Action.
      • Resource: "*".
  4. Prioritize:
    • Policies with Action = "*" and Resource = "*".
    • Policies that allow specific escalation actions (list in section 1).

4. Analyze a Specific Role for Escalation Path​

To deep dive into one role:

  1. IAM console → Roles.
  2. Click a role name to inspect it.
  3. Tabs to check:
    • Permissions:
      • Look at each Attached policy and any Inline policies.
      • Click policy name → JSON tab.
    • Trust relationships:
      • Check which principals can assume this role (sts:AssumeRole).

For each attached/inline policy, look for:

  • Problematic Actions:

    Common privilege-escalation enablers:

    • iam:AttachUserPolicy
    • iam:AttachGroupPolicy
    • iam:AttachRolePolicy
    • iam:PutUserPolicy
    • iam:PutGroupPolicy
    • iam:PutRolePolicy
    • iam:CreatePolicyVersion
    • iam:SetDefaultPolicyVersion
    • iam:PassRole
    • sts:AssumeRole
    • iam:CreateAccessKey
    • iam:UpdateAccessKey
    • iam:CreateLoginProfile
    • iam:UpdateLoginProfile
  • Broad Resources:

    • Resource: "*" on the above actions.
    • Or resources that cover admin roles/policies.

If a role a user has can assume another highly privileged role (via sts:AssumeRole), that is a privilege escalation path.


5. Remediate Policies (Least-Privilege Hardening)​

5.1 Remove or Restrict Dangerous IAM Actions​

For each risky policy:

  1. IAM → Policies → search for the policy name.

  2. Open policy → Permissions tab → Edit.

  3. In JSON or Visual editor, do the following:

    a) Remove unneeded IAM-write actions completely
    For example, remove:

    • iam:Attach*
    • iam:Put*
    • iam:CreatePolicyVersion
    • iam:SetDefaultPolicyVersion
    • iam:CreateAccessKey
    • iam:UpdateAccessKey
    • iam:CreateLoginProfile
    • iam:UpdateLoginProfile

    b) Where iam:PassRole is required:

    • Restrict Resource to specific safe roles, not "*".
    • Add conditions like:
      "Condition": {
      "StringEquals": {
      "iam:PassedToService": [
      "ecs.amazonaws.com",
      "lambda.amazonaws.com"
      ]
      }
      }

    c) Where sts:AssumeRole is required:

    • Restrict to only the specific role ARNs needed.
    • Do not allow "*" for resources.
  4. Click Next, review, then Save changes.

If an AWS-managed policy is risky (you can’t edit it):

  • Detach it from users/roles and replace with a custom, scoped-down customer-managed policy:
    1. Policies → Create policy.
    2. Define least-privilege permissions.
    3. Create policy, then Attach it to the target role/user.
    4. Detach the AWS-managed policy from the entity.

5.2 Tighten Trust Policies (Who Can Assume the Role)​

  1. IAM → Roles → choose role → Trust relationships tab.

  2. Click Edit trust policy.

  3. Check for:

    • "Principal": "*" or overly broad accounts.
    • Conditions missing on sts:AssumeRole.
  4. Adjust to only allow required principals and, ideally, require conditions. Example (only one account and specific role):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/AppRole" },
"Action": "sts:AssumeRole"
}
]
}

Avoid letting low-privilege roles, wildcards, or external accounts assume highly privileged roles.


6. Use the IAM Policy Simulator to Validate​

  1. Go to IAM → Policy Simulator.
  2. Choose User or Role you want to test.
  3. Select services like IAM, STS, Lambda, EC2.
  4. Test specific actions:
    • iam:PassRole, iam:AttachRolePolicy, sts:AssumeRole, iam:CreateAccessKey, etc.
  5. Confirm:
    • Only the minimal, intended actions are allowed.
    • Escalation-enabling combinations are not possible.

7. Add Guardrails: SCPs and Permissions Boundaries (If Using Organizations)​

If you use AWS Organizations:

  1. Service Control Policies (SCPs):

    • AWS Organizations console → Policies → Service control policies.
    • Create SCPs that deny dangerous actions for all non-admin accounts, e.g.:
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Effect": "Deny",
    "Action": [
    "iam:CreatePolicyVersion",
    "iam:SetDefaultPolicyVersion",
    "iam:AttachRolePolicy",
    "iam:PassRole"
    ],
    "Resource": "*"
    }
    ]
    }
    • Attach SCP to appropriate OUs/accounts.
    • Exclude a tightly-controlled admin account if necessary.
  2. Permissions boundaries (optional, more advanced):

    • Create a boundary policy that excludes privilege-escalation actions.
    • Attach as permission boundary to roles developers can create.

8. Continuous Monitoring​

  1. IAM console → Access Analyzer → Analyzer:
    • Enable analyzers to detect broad or external access.
  2. IAM → Credential report:
    • Periodically review user access keys, console access.
  3. Enable CloudTrail and (optionally) CloudTrail Lake / GuardDuty:
    • Monitor for suspicious IAM activity (e.g., new CreatePolicyVersion, SetDefaultPolicyVersion, AttachRolePolicy).

If you share a specific risky role/policy JSON, I can give exact edits line-by-line for remediation.

Using CLI

Below is a practical, CLI‑focused process to identify and fix IAM role permissions that allow privilege escalation in AWS.


1. Know the risky IAM permissions​

These actions are commonly used for privilege escalation when overly broad:

  • iam:PassRole
  • iam:PutRolePolicy
  • iam:AttachRolePolicy
  • iam:PutUserPolicy
  • iam:CreatePolicyVersion
  • iam:SetDefaultPolicyVersion
  • iam:UpdateAssumeRolePolicy
  • iam:AddUserToGroup, iam:AttachGroupPolicy
  • sts:AssumeRole
  • sts:AssumeRoleWithWebIdentity

Your goal: find any role that can call these actions in a dangerous way (e.g., unscoped to specific resources, or to Admin policies/roles).


2. Enumerate IAM roles and their policies​

# List all roles
aws iam list-roles --query 'Roles[*].RoleName' --output text

For each role (or for ones you suspect):

ROLE_NAME=YourRoleName

# Inline policies on the role
aws iam list-role-policies --role-name "$ROLE_NAME"

# Get contents of each inline policy
aws iam get-role-policy \
--role-name "$ROLE_NAME" \
--policy-name YourInlinePolicyName

# Attached managed policies
aws iam list-attached-role-policies --role-name "$ROLE_NAME"

# For each attached policy, get its default policy document
POLICY_ARN=arn:aws:iam::aws:policy/YourPolicy
DEFAULT_VERSION=$(aws iam get-policy \
--policy-arn "$POLICY_ARN" \
--query 'Policy.DefaultVersionId' \
--output text)

aws iam get-policy-version \
--policy-arn "$POLICY_ARN" \
--version-id "$DEFAULT_VERSION"

Look in the policy JSON for Action containing the risky IAM actions above.


3. Identify privilege‑escalation patterns​

Flags to look for in the policy JSON:

  1. Wildcard actions:
    • "Action": "iam:*" or "*" on IAM / STS
  2. Unrestricted PassRole:
    • "Action": "iam:PassRole" with "Resource": "*"
  3. Policy modification on high‑privilege policies:
    • iam:AttachRolePolicy, iam:PutRolePolicy, iam:CreatePolicyVersion, iam:SetDefaultPolicyVersion with "Resource": "*"
  4. Trust policies that are too broad:
    • sts:AssumeRole allowed from Principal: "*", or from untrusted accounts/services.

To view a role’s trust policy:

aws iam get-role --role-name "$ROLE_NAME" \
--query 'Role.AssumeRolePolicyDocument' \
--output json

4. Remove or scope dangerous permissions (inline policies)​

4.1. Download and edit the inline policy locally​

ROLE_NAME=YourRoleName
POLICY_NAME=YourInlinePolicyName

aws iam get-role-policy \
--role-name "$ROLE_NAME" \
--policy-name "$POLICY_NAME" \
--query 'PolicyDocument' \
--output json > policy.json

Edit policy.json:

  • Remove unneeded risky actions (e.g., iam:*, iam:PassRole, etc.).
  • Or narrow them:
    • Restrict Resource to only specific ARNs (e.g., just one role that truly needs to be passed).
    • Add Condition (e.g., limit iam:PassRole to certain services).

Example: scope iam:PassRole to a single role:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPassSpecificRoleToEC2",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/EC2LimitedRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "ec2.amazonaws.com"
}
}
}
]
}

Re‑apply the edited inline policy:

aws iam put-role-policy \
--role-name "$ROLE_NAME" \
--policy-name "$POLICY_NAME" \
--policy-document file://policy.json

5. Replace or detach risky managed policies​

If a role has broad AWS‑managed or customer‑managed policies (e.g., AdministratorAccess, custom *:* policies):

ROLE_NAME=YourRoleName
POLICY_ARN=arn:aws:iam::aws:policy/AdministratorAccess

# Detach the overly permissive policy
aws iam detach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn "$POLICY_ARN"

Create a least‑privilege customer‑managed policy:

cat > least-priv-role-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AppNeedsOnlyTheseActions",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
EOF

aws iam create-policy \
--policy-name MyLeastPrivPolicy \
--policy-document file://least-priv-role-policy.json

NEW_POLICY_ARN=$(aws iam list-policies \
--scope Local \
--query "Policies[?PolicyName=='MyLeastPrivPolicy'].Arn" \
--output text)

aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn "$NEW_POLICY_ARN"

6. Lock down trust policies (AssumeRole)​

For any role that can lead to higher privilege (e.g., admin roles):

  1. Restrict who can assume it.
  2. Avoid Principal: "*".
  3. Limit to specific AWS accounts, roles, or services.

Get and edit trust policy:

ROLE_NAME=PrivilegedRole

aws iam get-role --role-name "$ROLE_NAME" \
--query 'Role.AssumeRolePolicyDocument' \
--output json > trust.json

Example safer trust policy (from a specific account/role only):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/ApprovedCallerRole"
},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
]
}

Apply:

aws iam update-assume-role-policy \
--role-name "$ROLE_NAME" \
--policy-document file://trust.json

7. Fix dangerous policy versioning rights​

If a role can call iam:CreatePolicyVersion or iam:SetDefaultPolicyVersion on sensitive policies, an attacker can swap in an admin version.

For customer‑managed policies:

  1. List who can modify them.
  2. Remove those actions, or scope them with conditions (e.g., only to non‑privileged policies, or only a CI/CD role).

To reduce rights, edit the responsible role’s policy as in step 4 and remove:

  • iam:CreatePolicyVersion
  • iam:SetDefaultPolicyVersion
  • iam:DeletePolicyVersion

Then re‑apply with put-role-policy.


8. Validate with the Policy Simulator​

Before and after changes, you can check what a role can do:

ROLE_ARN=arn:aws:iam::123456789012:role/YourRole

aws iam simulate-principal-policy \
--policy-source-arn "$ROLE_ARN" \
--action-names iam:PassRole iam:CreatePolicyVersion iam:AttachRolePolicy sts:AssumeRole \
--output json

Verify that high‑risk actions are explicitDeny or not allowed.


9. Optional: enforce org‑wide via SCPs (if using AWS Organizations)​

At the org/root or OU level, create SCPs that deny dangerous privilege‑escalation patterns except for tightly controlled admin roles:

Example SCP snippet:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPassRoleToAnyRoleExceptAdminControllers",
"Effect": "Deny",
"Action": "iam:PassRole",
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": [
"arn:aws:iam::*:role/OrgAdminRole"
]
}
}
}
]
}

Apply with:

aws organizations create-policy \
--content file://scp.json \
--name RestrictPassRole \
--type SERVICE_CONTROL_POLICY
# then attach to OU or root

If you want, you can paste one specific role’s current policy JSON, and I can show you exactly how to rewrite it to remove the privilege‑escalation paths and give you the exact aws cli commands to apply the fix.

Using Python

Below is a practical, step‑by‑step remediation approach for AWS IAM privilege‑escalation risks, with Python (boto3) examples.


1. Understand the Common IAM Privilege Escalation Paths​

Typical dangerous permissions (especially if a user/principal can call them on themselves or roles they assume):

  • iam:CreatePolicyVersion (with SetAsDefault=true)
  • iam:PutRolePolicy, iam:PutUserPolicy, iam:AttachRolePolicy, iam:AttachUserPolicy, iam:AttachGroupPolicy
  • iam:PassRole (especially combined with EC2, Lambda, Step Functions, etc.)
  • sts:AssumeRole
  • Permissions on Lambda, EC2, Glue, Step Functions, etc., that allow attaching or modifying roles/policies:
    • lambda:UpdateFunctionConfiguration (to change execution role)
    • ec2:RunInstances with instance profile you can control
    • glue:CreateDevEndpoint with role you can pass
  • Wildcards like "Action": "*", "Resource": "*" (especially in combination).

Remediation = remove or restrict these where they are not explicitly needed, and add guardrails (permission boundaries, SCPs, condition keys).


2. Enumerate IAM Roles and Policies (Python/boto3)​

import boto3
import json

iam = boto3.client('iam')

def list_all_roles():
roles = []
paginator = iam.get_paginator('list_roles')
for page in paginator.paginate():
roles.extend(page['Roles'])
return roles

def list_attached_policies_for_role(role_name):
attached = []
paginator = iam.get_paginator('list_attached_role_policies')
for page in paginator.paginate(RoleName=role_name):
attached.extend(page['AttachedPolicies'])
return attached

def list_inline_policies_for_role(role_name):
policies = []
paginator = iam.get_paginator('list_role_policies')
for page in paginator.paginate(RoleName=role_name):
policies.extend(page['PolicyNames'])
return policies

3. Identify High‑Risk Permissions in Policies​

Define a set of “risky” actions and scan all attached + inline policies.

RISKY_ACTIONS = {
"iam:CreatePolicyVersion",
"iam:PutRolePolicy",
"iam:PutUserPolicy",
"iam:AttachRolePolicy",
"iam:AttachUserPolicy",
"iam:AttachGroupPolicy",
"iam:PassRole",
"iam:PutRolePermissionsBoundary",
"iam:PutUserPermissionsBoundary",
"sts:AssumeRole",
"*", # catch-all, will treat carefully
}

def get_policy_document_arn(policy_arn, version_id=None):
if not version_id:
policy = iam.get_policy(PolicyArn=policy_arn)
version_id = policy['Policy']['DefaultVersionId']
version = iam.get_policy_version(PolicyArn=policy_arn, VersionId=version_id)
return version['PolicyVersion']['Document']

def get_inline_policy_document(role_name, policy_name):
resp = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
return resp['PolicyDocument']

def statement_contains_risky_actions(statement):
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
actions = [a.lower() for a in actions]
risky = []
for a in actions:
if a == "*" or a in {x.lower() for x in RISKY_ACTIONS}:
risky.append(a)
return risky

def find_risky_perms_in_policy(document):
risky_statements = []
statements = document.get("Statement", [])
if isinstance(statements, dict):
statements = [statements]
for stmt in statements:
risky = statement_contains_risky_actions(stmt)
if risky:
risky_statements.append((stmt, risky))
return risky_statements

Usage: loop through roles and attached policies:

def audit_roles_for_priv_esc():
roles = list_all_roles()
findings = []

for role in roles:
role_name = role['RoleName']

# Managed policies
attached = list_attached_policies_for_role(role_name)
for ap in attached:
doc = get_policy_document_arn(ap['PolicyArn'])
risky_stmts = find_risky_perms_in_policy(doc)
if risky_stmts:
findings.append({
"PrincipalType": "Role",
"PrincipalName": role_name,
"PolicyType": "Managed",
"PolicyArn": ap['PolicyArn'],
"RiskyStatements": risky_stmts,
})

# Inline policies
inline_names = list_inline_policies_for_role(role_name)
for pname in inline_names:
doc = get_inline_policy_document(role_name, pname)
risky_stmts = find_risky_perms_in_policy(doc)
if risky_stmts:
findings.append({
"PrincipalType": "Role",
"PrincipalName": role_name,
"PolicyType": "Inline",
"PolicyName": pname,
"RiskyStatements": risky_stmts,
})

return findings

if __name__ == "__main__":
findings = audit_roles_for_priv_esc()
print(json.dumps(findings, default=str, indent=2))

This identifies where privilege‑escalation‑prone actions exist.


4. Decide on Remediation Strategy (Per Finding)​

Per role/policy combination, you should:

  1. Confirm business need for each risky action.
  2. If not needed:
    • Remove or narrow the statement.
  3. If needed:
    • Restrict resources.
    • Add IAM conditions.
    • Use permission boundaries and/or SCPs.
    • Separate “break‑glass” roles from normal operators.

Below are concrete policy changes and how to apply them via Python.


5. Remediate Excessive iam:PassRole​

5.1 Policy Before (too broad)​

{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "*"
}

5.2 Policy After (restricted roles + conditions)​

{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::123456789012:role/SpecificServiceRole"
],
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
}

5.3 Update Policy Version via Python​

For managed policies:

def create_restricted_policy_version(policy_arn, new_document):
resp = iam.create_policy_version(
PolicyArn=policy_arn,
PolicyDocument=json.dumps(new_document),
SetAsDefault=True
)
return resp['PolicyVersion']['VersionId']

Typical flow:

  1. Get existing document.
  2. Manually or programmatically modify statements (e.g., change Resource: "*" to specific ARNs; add Condition).
  3. Call create_policy_version with SetAsDefault=True.
  4. Optionally delete old versions later (max 5).

6. Remove or Restrict Delegated Policy Management (iam:*Policy*)​

If a role has permissions like iam:CreatePolicyVersion, iam:PutRolePolicy, etc., and it’s not a dedicated IAM admin role:

  1. Create a new, least‑privilege IAM admin role for identity team only.
  2. Remove those actions from operational roles.

Sample adjustment:

Before:

{
"Effect": "Allow",
"Action": [
"iam:CreatePolicyVersion",
"iam:PutRolePolicy",
"iam:AttachRolePolicy"
],
"Resource": "*"
}

After (if minimal is needed):

If the role only needs to attach a pre‑approved policy to a specific role:

{
"Effect": "Allow",
"Action": [
"iam:AttachRolePolicy"
],
"Resource": "arn:aws:iam::123456789012:role/SpecificRole",
"Condition": {
"StringEquals": {
"iam:PolicyARN": "arn:aws:iam::123456789012:policy/PreApprovedPolicy"
}
}
}

Apply via same create_policy_version mechanism.

For inline policies, update with put_role_policy:

def update_inline_policy(role_name, policy_name, new_document):
iam.put_role_policy(
RoleName=role_name,
PolicyName=policy_name,
PolicyDocument=json.dumps(new_document)
)

7. Constrain sts:AssumeRole and Trust Policies​

Even if role A doesn’t have broad iam:*, it can escalate by assuming a more‑privileged role B.

7.1 Inspect trust policies​

def get_role_trust_policy(role_name):
role = iam.get_role(RoleName=role_name)
return role['Role']['AssumeRolePolicyDocument']

def update_role_trust_policy(role_name, new_trust_doc):
iam.update_assume_role_policy(
RoleName=role_name,
PolicyDocument=json.dumps(new_trust_doc)
)

7.2 Restrict trust policies​

  • Use specific principals, not *.
  • Add conditions like aws:PrincipalTag, aws:SourceAccount, aws:SourceArn.

Before (too open):

{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "sts:AssumeRole"
}

After (restricted):

{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/SpecificCallerRole" },
"Action": "sts:AssumeRole"
}

Or with conditions:

{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:PrincipalTag/RoleType": "Automation" }
}
}

8. Guardrails: Permission Boundaries and SCPs (Org‑Level)​

8.1 Permission Boundaries (per principal)​

Create a boundary policy that forbids escalation actions (or limits them tightly), then attach it as a permission boundary to roles/users that you do not want to be able to escalate.

Example permission boundary:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"iam:CreatePolicyVersion",
"iam:PutUserPolicy",
"iam:PutRolePolicy",
"iam:AttachUserPolicy",
"iam:AttachRolePolicy",
"iam:PassRole"
],
"Resource": "*"
}
]
}

Attach via Python:

def attach_permission_boundary_to_role(role_name, boundary_arn):
iam.update_role(
RoleName=role_name,
PermissionsBoundary=boundary_arn
)

8.2 Service Control Policies (SCPs) (Org‑wide)​

In AWS Organizations, SCPs can deny dangerous actions account‑wide except for a specific IAM admin role.

Example (pseudo; must be applied via Organizations API/console):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"iam:CreatePolicyVersion",
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"iam:PassRole"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/CentralIAMAdmin"
}
}
}
]
}

9. Special Case: Service‑Based Escalation (Lambda, EC2, etc.)​

Even if iam:PassRole is restricted, some combos can still escalate; for each:

  • Lambda: lambda:UpdateFunctionConfiguration to change Role.
  • EC2: ec2:RunInstances with a modifiable instance profile.
  • Step Functions: states:CreateStateMachine with high‑priv role.

Remediation pattern:

  • Constrain Resource to specific functions, instances, state machines.
  • Combine with tight iam:PassRole (only low‑priv roles).
  • Use conditions like iam:PassedToService, ec2:InstanceProfile, or tags.

Example for Lambda:

{
"Effect": "Allow",
"Action": [
"lambda:UpdateFunctionConfiguration"
],
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:MyApp-*",
"Condition": {
"StringEquals": {
"lambda:Role": "arn:aws:iam::123456789012:role/MyAppExecutionRole"
}
}
}

10. Verification / Regression Checks​

After changes:

  1. Use IAM Access Analyzer (console or API) to detect potential privilege escalation.
  2. Re‑run your Python audit script to ensure:
    • No unexpected iam:*Policy* / iam:PassRole / sts:AssumeRole wildcards.
    • No Action: "*" + Resource: "*" for non‑admin roles.
  3. Test critical workflows to confirm business operations continue to function.

If you want, I can:

  • Help you adapt the audit script to automatically suggest a “patched” policy document, or
  • Focus on a specific privileged role’s policies and walk through concrete JSON diffs.
Using Terraform
# IAM policy with privilege‑escalation actions removed or tightly scoped
resource "aws_iam_policy" "restricted_role_management" {
name = "restricted-role-management"
description = "Allows only tightly scoped role management actions without privilege escalation"

# Replace with the ARN of the specific role this workload is allowed to pass
# e.g. arn:aws:iam::123456789012:role/my-allowed-target-role
# Substitute YOUR_ACCOUNT_ID and YOUR_ROLE_NAME as needed.
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# Example: allow passing only a specific role, and only to a specific service
{
Sid = "AllowPassSpecificRoleToSpecificService"
Effect = "Allow"
Action = [
"iam:PassRole"
]
Resource = "arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ALLOWED_TARGET_ROLE"
Condition = {
StringEquals = {
"iam:PassedToService" = "ecs-tasks.amazonaws.com" # SUBSTITUTE TARGET SERVICE
}
}
},

# Example: allow listing/reading IAM metadata, which is not escalating
{
Sid = "AllowIamReadOnly"
Effect = "Allow"
Action = [
"iam:GetRole",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies",
"iam:ListRoles"
]
Resource = "*"
}

# NOTE:
# - DO NOT include broad write actions like:
# iam:AttachRolePolicy
# iam:PutRolePolicy
# iam:CreatePolicy
# iam:CreatePolicyVersion
# iam:SetDefaultPolicyVersion
# iam:UpdateAssumeRolePolicy
# iam:AddUserToGroup
# iam:UpdateLoginProfile
# sts:AssumeRole on higher-privilege roles
# - If such actions are currently in the JSON, remove them or scope them
# to the exact roles/policies this workload must manage, with explicit ARNs.
]
})
}

# Attach the remediated policy to the role that previously had an escalating policy
resource "aws_iam_role_policy_attachment" "role_restricted_attachment" {
role = aws_iam_role.TARGET_ROLE.name # SUBSTITUTE EXISTING ROLE
policy_arn = aws_iam_policy.restricted_role_management.arn
}

# Example role showing how it would be referenced (if not already defined)
resource "aws_iam_role" "TARGET_ROLE" {
name = "YOUR_EXISTING_ROLE_NAME" # SUBSTITUTE; align with actual role name

assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

data "aws_iam_policy_document" "assume_role_policy" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"] # SUBSTITUTE CALLER SERVICE
}
}
}

Changing an existing aws_iam_policy in Terraform (rather than editing inline JSON only) forces replacement of the policy resource; any existing attachments will be recreated by Terraform, which may briefly remove and reattach the policy during apply.

For verification, terraform plan should show: removal of the old IAM policy resource (with privilege‑escalation actions), creation of the new restricted aws_iam_policy, and updates to aws_iam_role_policy_attachment resources so that the role is attached only to the remediated policy.