AWS IAM Hardening: Eliminating the Over-Privileged Role Problem
In almost every AWS environment I'm brought into, the same problem appears within the first hour of review: IAM has grown organically, nobody owns it, and half the roles have far more access than they need. A developer needed S3 access two years ago, got AdministratorAccess because it was faster, and that role has been sitting there ever since. Multiply that across a team of 20, add a few CI/CD pipelines and Lambda functions, and you have an access model that would fail any serious security audit — and give an attacker lateral movement across your entire account if a single credential is compromised.
This article is a practical walkthrough of how I approach IAM hardening engagements: auditing what exists, eliminating the risk, and building a sustainable access model that doesn't require a dedicated IAM team to maintain.
IAM misconfiguration is consistently one of the top causes of AWS breaches. The permissions are usually correct for the task — they're just never scoped down after the task is done. The fix is process, not technology.
1. Audit First — Know What You're Dealing With
Before changing anything, you need a complete picture. AWS provides the tools — most environments just never use them. Start by generating a credential report and an IAM Access Analyzer finding:
# Generate the IAM credential report
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d > iam-credential-report.csv
# Enable IAM Access Analyzer (if not already active)
aws accessanalyzer create-analyzer \
--analyzer-name prod-analyzer \
--type ACCOUNT
# Pull all findings
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:ACCOUNT_ID:analyzer/prod-analyzer \
--query 'findings[?status==`ACTIVE`]' \
--output table
The credential report tells you which users have passwords, active access keys, when those keys were last used, and whether MFA is enabled. In most environments this report alone surfaces 5–10 critical findings: root access keys still active, users with passwords and no MFA, access keys that haven't been used in over 90 days and should be rotated or deleted.
Find the blast radius of each role
For each role, you want to understand what it can actually do — not just what policies are attached. AWS's IAM Policy Simulator is useful for spot checks, but for a full audit I use a combination of the CLI and a quick Python script to flatten all attached and inline policies:
# List all roles and their attached policies
aws iam list-roles --query 'Roles[*].[RoleName,Arn]' --output table
# For each role, list attached managed policies
aws iam list-attached-role-policies --role-name YOUR_ROLE_NAME
# List inline policies (these are often missed)
aws iam list-role-policies --role-name YOUR_ROLE_NAME
# Check who can assume the role (trust policy)
aws iam get-role --role-name YOUR_ROLE_NAME \
--query 'Role.AssumeRolePolicyDocument'
Pay close attention to trust policies. A role that can be assumed by *, or by any principal in the account without conditions, is a privilege escalation path waiting to be exploited.
2. Identify and Eliminate Wildcard Permissions
Wildcard permissions — "Action": "*" or "Resource": "*" — are the most common finding in under-governed AWS accounts. AWS-managed policies like AdministratorAccess and PowerUserAccess are convenient but should never be attached to anything other than break-glass emergency roles with strict conditions on their use.
# Find all policies in your account with wildcard actions
aws iam list-policies --scope Local --query 'Policies[*].Arn' --output text | \
tr '\t' '\n' | while read arn; do
version=$(aws iam get-policy --policy-arn "$arn" \
--query 'Policy.DefaultVersionId' --output text)
doc=$(aws iam get-policy-version --policy-arn "$arn" \
--version-id "$version" \
--query 'PolicyVersion.Document' --output json)
if echo "$doc" | grep -q '"Action": "\*"'; then
echo "WILDCARD ACTION found in: $arn"
fi
done
Any policy returned by this scan needs to be replaced with a scoped equivalent. The replacement process looks like this:
- Identify what the role actually does. Check CloudTrail for the last 90 days of API calls made by this role — the
eventSourceandeventNamefields tell you exactly which actions are being used. - Write a policy scoped to those actions and resources. Use
arn:aws:s3:::specific-bucket-name/*instead ofarn:aws:s3:::*. - Test in a staging environment first. Attach the new policy, run the workload, check CloudTrail for any
AccessDeniedevents, and add only what's missing. - Deploy and monitor. Set up a CloudWatch alarm on
AccessDeniedevents for the role for 30 days to catch anything missed.
3. Use CloudTrail to Write Least-Privilege Policies
The fastest way to build an accurate least-privilege policy is to let the role run normally for a period, then use CloudTrail to see what it actually called. AWS IAM Access Analyzer can do this automatically with its policy generation feature:
# Start policy generation based on CloudTrail activity
aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::ACCOUNT_ID:role/YOUR_ROLE_NAME
# After a few seconds, retrieve the results
aws iam get-service-last-accessed-details \
--job-id JOB_ID_FROM_ABOVE \
--query 'ServicesLastAccessed[?TotalAuthenticatedEntities>`0`].[ServiceName,LastAuthenticated]' \
--output table
Any service that shows zero authenticated entities in the last 90 days should be removed from the role's policy entirely. This single step typically reduces the effective permission surface by 40–60% in mature environments where roles have accumulated permissions over time.
4. Enforce MFA and Credential Hygiene
For any human IAM user (which should ideally be zero in a mature AWS account — use IAM Identity Center instead), MFA must be enforced with an SCP or an IAM condition. A commonly missed pattern is attaching a deny policy that blocks all actions except the MFA enrollment steps when MFA is not present:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllWithoutMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
For access key hygiene, set up a Lambda function triggered on a schedule to scan for keys older than 90 days and either rotate them automatically or send an alert:
# IAM password policy — enforce via CLI
aws iam update-account-password-policy \
--minimum-password-length 16 \
--require-symbols \
--require-numbers \
--require-uppercase-characters \
--require-lowercase-characters \
--allow-users-to-change-password \
--max-password-age 90 \
--password-reuse-prevention 24 \
--hard-expiry
5. Service Control Policies (SCPs) at the Organization Level
If you're running AWS Organizations (and you should be), SCPs are your highest-leverage control. They act as guardrails that no IAM policy in any member account can override — even AdministratorAccess. A few SCPs that should be standard in any hardened environment:
# Deny root account usage (apply at OU level)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootUser",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
}
]
}
# Deny disabling CloudTrail
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCloudTrailDisable",
"Effect": "Deny",
"Action": [
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging",
"cloudtrail:UpdateTrail"
],
"Resource": "*"
}
]
}
The CloudTrail SCP is particularly important — an attacker who gains admin access in an account will often try to disable logging to cover their tracks. With this SCP, they can't, regardless of their IAM permissions.
6. Automate Continuous Compliance with AWS Config
IAM hardening is not a one-time event. Roles get created, policies get attached, and without continuous enforcement, drift is inevitable. AWS Config rules let you detect and alert on violations automatically:
# Enable key IAM-related Config rules via CLI
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "iam-root-access-key-check",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "IAM_ROOT_ACCESS_KEY_CHECK"
}
}'
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "iam-user-mfa-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "IAM_USER_MFA_ENABLED"
}
}'
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "access-keys-rotated",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "ACCESS_KEYS_ROTATED"
},
"InputParameters": "{\"maxAccessKeyAge\": \"90\"}"
}'
Pair these Config rules with SNS notifications and you have a continuous compliance loop: violations surface immediately rather than at the next quarterly review.
What a Hardened IAM Baseline Looks Like
After a thorough IAM engagement, the target state should look like this:
- No IAM users for workloads. EC2 instances, Lambda functions, ECS tasks, and CI/CD pipelines all use IAM roles with instance profiles or OIDC federation. No long-lived access keys in environment variables or code.
- No wildcard permissions in any customer-managed policy. Every policy scoped to specific actions and named resources.
- MFA enforced for all human access. IAM Identity Center configured with your IdP for SSO; direct IAM user access eliminated or restricted to break-glass scenarios.
- CloudTrail enabled in all regions, logs shipped to a dedicated security account. No workload account has write access to the log bucket.
- SCPs deployed at the OU level preventing root usage, CloudTrail disabling, and region expansion beyond approved regions.
- AWS Config rules monitoring for drift, with alerts firing to your security channel within minutes of a violation.
An auditor should be able to look at your IAM configuration and understand exactly who can do what, why they need it, and how you'd know if something changed. If you can't answer those three questions, the work isn't done.
Getting to this state in a live AWS account with years of accumulated access takes a methodical approach — you can't revoke permissions without understanding the blast radius. If you'd like help auditing and hardening your AWS IAM setup, or if you're preparing for a SOC 2, PCI-DSS, or FedRAMP audit and need your access model to be defensible, reach out. This is a core part of what I do.
I audit and remediate AWS IAM for teams preparing for security reviews, compliance audits, or who simply want to know their access model is airtight.