How to Audit AWS IAM Role Usage: Programmatic Methods to Identify Unused Roles


2 views

In complex AWS environments, IAM roles often proliferate without proper tracking. These unused roles create security risks and clutter IAM management consoles. Manual verification becomes impossible when dealing with hundreds of roles across multiple accounts.

Enable CloudTrail logging if not already active. Then query for role assumption events:

aws cloudtrail lookup-events \
    --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
    --start-time $(date -d "-90 days" +%s) \
    --end-time $(date +%s) \
    --query "Events[].CloudTrailEvent" \
    --output text | jq -r '.userIdentity.arn' | sort | uniq -c

Generate an IAM access report for role last-used information:

aws iam generate-service-last-accessed-details \
    --arn arn:aws:iam::123456789012:role/MySuspiciousRole

Then retrieve the report:

aws iam get-service-last-accessed-details \
    --job-id JOB_ID_FROM_PREVIOUS_COMMAND

While not direct evidence, unused roles typically show no associated costs. Check AWS Cost Explorer with these filters:

  • "Service" = "IAM"
  • "Usage Type" = "RoleUsage"
  • Time range = last 6 months

For bulk identification of potentially unused roles:

#!/bin/bash

for role in $(aws iam list-roles --query 'Roles[].RoleName' --output text)
do
    last_used=$(aws iam get-role --role-name $role \
        --query 'Role.RoleLastUsed.LastUsedDate' --output text)
    
    if [ -z "$last_used" ]; then
        echo "$role - NEVER USED"
    else
        days_since=$(( ($(date +%s) - $(date -d "$last_used" +%s)) / 86400 ))
        echo "$role - Last used $days_since days ago"
    fi
done

Implement a standardized tagging approach for all new roles:

aws iam tag-role \
    --role-name NewApplicationRole \
    --tags Key=Owner,Value=team@example.com Key=Created,Value=$(date +%Y-%m-%d)

Consider tools like:

  • AWS Trusted Advisor (for Business/Enterprise support plans)
  • CloudCheckr or CloudHealth for cross-account analysis
  • Open-source tools like CloudMapper or aws-iam-age

During AWS account maintenance, discovering unused IAM roles is like finding abandoned keys - you're never quite sure if they're still needed. Manual verification becomes impossible at scale, especially in accounts with hundreds of services and cross-account access patterns.

The most reliable method is analyzing CloudTrail logs. Every AssumeRole API call leaves a trail we can query:

import boto3
from datetime import datetime, timedelta

def check_role_usage(role_name, lookback_days=90):
    cloudtrail = boto3.client('cloudtrail')
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(days=lookback_days)
    
    response = cloudtrail.lookup_events(
        LookupAttributes=[
            {'AttributeKey': 'EventName', 'AttributeValue': 'AssumeRole'},
            {'AttributeKey': 'ResourceName', 'AttributeValue': role_name}
        ],
        StartTime=start_time,
        EndTime=end_time,
        MaxResults=50
    )
    
    return len(response.get('Events', [])) > 0

AWS maintains metadata about when roles were last accessed:

def get_last_used(role_name):
    iam = boto3.client('iam')
    try:
        response = iam.get_role(RoleName=role_name)
        last_used = response['Role']['RoleLastUsed']
        return {
            'last_used_date': last_used.get('LastUsedDate'),
            'region': last_used.get('Region')
        }
    except Exception as e:
        print(f"Error checking {role_name}: {str(e)}")
        return None

Some roles might be used indirectly through service-linked roles or in rare scenarios. For comprehensive checks:

  1. Scan all Lambda functions for execution roles
  2. Check EC2 instance profiles
  3. Review ECS task definitions
  4. Examine CloudFormation stack roles

For production environments, implement a phased approach:

def safe_role_cleanup(role_name):
    usage_data = get_last_used(role_name)
    
    if not usage_data or usage_data['last_used_date'] < (datetime.now() - timedelta(days=180)):
        print(f"Role {role_name} appears unused - detaching policies")
        iam = boto3.client('iam')
        
        # First remove permissions
        for policy in iam.list_attached_role_policies(RoleName=role_name)['AttachedPolicies']:
            iam.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn'])
        
        # Then delete the role after monitoring
        print(f"Would delete {role_name} in production after verification")
        # iam.delete_role(RoleName=role_name)
    else:
        print(f"Role {role_name} was last used on {usage_data['last_used_date']}")

For accounts with numerous roles, generate a CSV report:

import csv

def generate_role_report(output_file):
    iam = boto3.client('iam')
    with open(output_file, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['Role Name', 'Last Used', 'Region', 'Days Inactive'])
        
        for role in iam.list_roles()['Roles']:
            last_used = role.get('RoleLastUsed', {})
            last_used_date = last_used.get('LastUsedDate')
            
            if last_used_date:
                inactive_days = (datetime.now(last_used_date.tzinfo) - last_used_date).days
            else:
                inactive_days = "Never"
                
            writer.writerow([
                role['RoleName'],
                last_used_date or "Never",
                last_used.get('Region', 'N/A'),
                inactive_days
            ])