Back to blog
AWSBest PracticesCloud SecurityIdentity & AccessOperations & Compliance

IAM Role Unused for 90+ Days: Find and Remove Stale AWS Roles

Stale IAM roles widen your AWS attack surface. Learn how to detect roles unused for 90+ days, remove them safely, and prevent them from piling up again.

TL;DR

This check flags IAM roles that haven't been used in 90 days or more. Stale roles widen your attack surface and clutter your permission model, so review them and delete the ones nobody owns. The quick fix: confirm the role is truly unused, then run aws iam delete-role after detaching its policies.

Every IAM role you create is a door into your AWS account. Some of those doors get used constantly. Others were opened for a one-off migration in 2021 and never closed. The IAM Role Unused for 90+ Days check finds those forgotten doors so you can decide whether to keep them locked or take them off the wall entirely.

This is one of the less glamorous parts of cloud security, but it's also one of the highest-leverage. Unused roles rarely get monitored, rarely get reviewed, and almost always carry more permissions than anyone remembers granting.


What this check detects

Lensix flags any IAM role whose last-used timestamp is more than 90 days in the past, or roles that have a creation date older than 90 days with no recorded usage at all. AWS tracks this through the RoleLastUsed field, which records the last time the role was assumed and the region where that happened.

The check covers all role types in your account: roles assumed by EC2 instances and Lambda functions, roles used for cross-account access, service-linked roles, and roles tied to federated or SSO identities.

Note: The RoleLastUsed tracking only goes back to a point in 2019 when AWS introduced the feature. A role created and used before then but never since will show no last-used data. AWS also only tracks activity within a 400-day window, so an empty value can mean "never used" or "not used in over 400 days."


Why it matters

An unused role is not neutral. It's a liability that sits quietly in your account until something goes wrong.

It expands your attack surface

Roles can be assumed by anyone or anything that meets their trust policy. A cross-account role with an overly broad trust relationship, for example one that trusts an entire account ID with no external ID, can be assumed by any principal in that account. If that other account is compromised, your unused role becomes an entry point you forgot existed.

Stale roles accumulate dangerous permissions

The role that was created for a temporary admin task often keeps its admin policy long after the task is done. Nobody removes permissions from a role they've forgotten about. When an attacker enumerates your IAM, they look specifically for roles like this: high privilege, low visibility, no one watching.

Warning: Service-linked roles and roles referenced by active automation can show as unused even when they're needed. Deleting one of these can break a service in ways that are not obvious until the next time it tries to run. Always confirm a role is genuinely orphaned before removing it.

It complicates audits and compliance

Frameworks like SOC 2, PCI DSS, and ISO 27001 expect you to enforce least privilege and review access regularly. A pile of stale roles makes every access review longer and every auditor more suspicious. Each one is a question you'll have to answer: who owns this, why does it exist, and why does it still have these permissions?


How to fix it

The remediation has two phases: verify the role is actually unused, then remove it cleanly.

Step 1: Check when the role was last used

Pull the last-used data directly so you're working from facts, not assumptions:

aws iam get-role --role-name my-old-role \
  --query 'Role.RoleLastUsed'

A response like this tells you the role hasn't been touched in the tracked window:

{}

While a populated response shows when and where it was last assumed:

{
  "LastUsedDate": "2023-08-14T09:22:00+00:00",
  "Region": "us-east-1"
}

Step 2: Find who and what depends on the role

Before deleting anything, understand the blast radius. Check the trust policy to see who can assume it:

aws iam get-role --role-name my-old-role \
  --query 'Role.AssumeRolePolicyDocument'

List the policies attached to the role so you know what you're removing:

# Managed policies
aws iam list-attached-role-policies --role-name my-old-role

# Inline policies
aws iam list-role-policies --role-name my-old-role

# Instance profiles that reference this role
aws iam list-instance-profiles-for-role --role-name my-old-role

Step 3: Detach policies and remove the role

Danger: Deleting an IAM role is irreversible, and any application, instance, or integration still relying on it will start failing immediately. Confirm ownership in writing before you run these commands against production. When in doubt, disable the role's trust policy first and wait a week to see what breaks.

A role can't be deleted while it still has attached policies or is part of an instance profile. Clean those up first:

# Detach each managed policy
aws iam detach-role-policy --role-name my-old-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

# Delete each inline policy
aws iam delete-role-policy --role-name my-old-role \
  --policy-name my-inline-policy

# Remove the role from any instance profile
aws iam remove-role-from-instance-profile \
  --instance-profile-name my-profile \
  --role-name my-old-role

# Finally, delete the role
aws iam delete-role --role-name my-old-role

Tip: Instead of deleting straight away, consider a "soft delete" pattern. Replace the role's trust policy with one that denies all assumption, tag it with pending-deletion and a date, then sweep up tagged roles after a grace period. This catches the role that turns out to be used by something quarterly.

The IaC equivalent

If the role was created through Terraform or CloudFormation, delete it there rather than out of band. Removing the resource block and applying keeps your state aligned with reality:

# Remove the aws_iam_role resource block from your .tf file, then:
terraform plan   # confirm only the intended role is destroyed
terraform apply

Deleting a managed role from the console while leaving it in your IaC will only recreate it on the next apply, so always close the loop in code.


How to prevent it from happening again

Cleaning up once is good. Making stale roles impossible to ignore is better.

Run a scheduled audit

Use the IAM credential and access advisor data on a schedule to surface roles that have gone quiet. A simple Lambda or scheduled job can pull every role's last-used date and post anything over your threshold to Slack:

aws iam list-roles --query 'Roles[].RoleName' --output text | \
while read role; do
  last=$(aws iam get-role --role-name "$role" \
    --query 'Role.RoleLastUsed.LastUsedDate' --output text)
  echo "$role last used: ${last:-NEVER}"
done

Tag roles with an owner and a purpose

The reason stale roles linger is that nobody knows who owns them. Enforce a tagging standard at creation time so every role carries an owner and purpose tag. Then your audit can route findings to a real person instead of a void.

Gate role creation in CI/CD with policy-as-code

Require ownership tags and a sensible permission boundary on every role before it merges. Here's a Checkov-style OPA/Rego idea expressed as a policy gate that rejects roles missing an owner tag:

# Example: fail the pipeline if any IAM role lacks an owner tag
checkov -d . --check CKV_AWS_TAG_OWNER \
  --compact --quiet

Tip: Pair the CI gate with attaching a permissions boundary to every role. Even if a role is forgotten, a tight boundary caps how much damage it can do if it's ever assumed by the wrong principal.

Let Lensix watch continuously

Manual sweeps drift. A continuous check re-evaluates every role on each scan, so a role that crosses the 90-day line shows up in your findings automatically instead of waiting for someone to remember to look.


Best practices

  • Prefer short-lived roles for humans. Use IAM Identity Center or federation so people get temporary credentials and you don't accumulate long-lived per-person roles that go stale.
  • Scope trust policies tightly. Cross-account roles should use an ExternalId condition and name specific principals, not whole account IDs.
  • Apply least privilege from day one. A role that only ever had the permissions it needed is far less dangerous when it goes unused.
  • Set a deletion threshold and stick to it. Ninety days is a reasonable default. Pick a number, document it, and automate enforcement so it isn't a judgment call every time.
  • Review service-linked roles separately. They follow different lifecycle rules and shouldn't be deleted on the same automated cadence as your custom roles.
  • Keep a record of deletions. Log what you removed and when. If something breaks two weeks later, you'll want to recreate the exact role quickly.

Unused roles are easy to create and easy to forget, which is exactly why they pile up. Treat them like any other expired credential: find them, verify them, and remove them on a schedule. Your attack surface shrinks, your audits get shorter, and your IAM stays something you can actually reason about.