The blast radius of a single IAM role
We drew the graph properly for the first time. It had edges nobody in the room could account for.
One role, ten paths
You’ve got an IAM role that grants s3:* on arn:aws:s3:::prod-data-*. Seems reasonable for the analytics team. But when we drew the dependency graph for the first time, we found something unexpected.
That single role had access to twelve downstream services. Each of which could create credentials. Which meant your blast radius wasn’t one role—it was an entire ecosystem.
{
"Role": "analytics-prod",
"DirectPermissions": ["s3:*"],
"BlastRadius": {
"services_with_credentials": 12,
"cross_account_access": true,
"assumable_by_external": ["lambda.amazonaws.com"]
}
}
The edges nobody saw
We mapped the permission graph properly using AWS IAM Access Analyzer. What we found:
- A Lambda function assumed this role and wrote results to a different S3 bucket
- That bucket had lifecycle policies that copied data to a disaster recovery account
- The DR account had permissions to restore deleted objects from our audit log bucket
One compromised credential meant we couldn’t prove what hadn’t been tampered with.
Drawing the graph right this time
# Simple script to enumerate role dependencies
import boto3
iam = boto3.client('iam')
role_policies = iam.list_role_policies(RoleName='analytics-prod')
for policy in role_policies['PolicyNames']:
print(f"\n{policy}:")
print(iam.get_role_policy(RoleName='analytics-prod', PolicyName=policy))
What we changed
- Principle of least privilege, actually: Split the monolithic role into twelve scoped roles
- Credential rotation on detection: Any anomaly triggers immediate rotation
- Graph-based auditing: Weekly runs of access analyzer to catch drift
- Human review of new edges: Any cross-account permission requires approval
The blast radius of a single IAM role is only as big as you’re willing to let it get. Draw the graph before the adversary does.