Find roles with wildcard actions or resources
Start with the policies attached to every role, and flag any statement using "Action": "*"
or "Resource": "*" outside of the handful of roles that legitimately need broad scope
(break-glass admin roles, CI deploy roles you've deliberately chosen to trust).
for role in $(aws iam list-roles --query 'Roles[].RoleName' --output text); do
policies=$(aws iam list-attached-role-policies --role-name "$role" \
--query 'AttachedPolicies[].PolicyArn' --output text)
for policy in $policies; do
version=$(aws iam get-policy --policy-arn "$policy" \
--query 'Policy.DefaultVersionId' --output text)
aws iam get-policy-version --policy-arn "$policy" --version-id "$version" \
--query 'PolicyVersion.Document.Statement[?Action==`*` || Resource==`*`]' \
--output json | grep -q '\[\]' || echo "REVIEW: $role -> $policy"
done
done
Find roles that haven't been used
IAM's own last-used data tells you which roles nothing has assumed recently. A role with broad permissions and no recent activity is pure downside: it grants an attacker capability without providing the business any benefit.
aws iam generate-service-last-accessed-details --arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME
# then, after a short wait for the report to generate:
aws iam get-service-last-accessed-details --job-id JOB_ID \
--query 'ServicesLastAccessed[?LastAuthenticated==`null`]'
Run this across every role older than 90 days. Anything with no last-authenticated date and standing permissions is a candidate for deletion, not just review.
Find trust policies that are wider than they look
A role's permissions only matter if you also know who can assume it. Check the trust policy
(AssumeRolePolicyDocument) on every role with meaningful permissions for a
Principal that's broader than intended: an entire account rather than a specific role,
or a federated identity provider condition that's missing an audience or subject restriction.
aws iam list-roles --query 'Roles[].[RoleName,AssumeRolePolicyDocument]' --output json \
| jq '.[] | select(.[1].Statement[].Principal.AWS == "*" or (.[1].Statement[].Principal.AWS | type == "string" and endswith(":root")))'
Prioritize what you find
- Wildcard action + broad trust policy + recent use: fix first. This is a live role with both the permission and the reachability to do real damage.
- Wildcard action + no recent use: delete or replace with a scoped policy before it's ever reused for something new.
- Narrow action + broad trust policy: tighten the trust policy. The permission is fine; who can assume it isn't.
- Everything else: document as accepted risk with an owner and a review date, rather than leaving it silently unreviewed.
What this satisfies
This maps directly to ISO 27001 Annex A 8.2 (privileged access rights) and is one of the first things a SOC 2 auditor samples for CC6.3 (role-based access review). Being able to show a repeatable process for finding and removing excess privilege, rather than a one-time cleanup, is what turns this into a control instead of a chore.
