Vesey Cyber Security

Over-privileged roles

Auditing IAM roles before they become the incident

Executive summary

Over-privileged roles rarely get created on purpose. They accumulate: a wildcard action added to unblock a deploy under time pressure and never narrowed afterward, a role built for one Lambda that gets reused for three more, a contractor's access that outlives the contract. None of this shows up until a single compromised credential turns into full account compromise instead of a contained incident. Below is a short CLI workflow for finding the worst of it in an afternoon, and a way to prioritize what you find.

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

  1. 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.
  2. Wildcard action + no recent use: delete or replace with a scoped policy before it's ever reused for something new.
  3. Narrow action + broad trust policy: tighten the trust policy. The permission is fine; who can assume it isn't.
  4. 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.

Want a second set of eyes on the results

A due diligence pack checks your actual IAM configuration against your customer's security questionnaire, and hands back a prioritized list of gaps.

← Back to all guides