Skip to main content

Overview

This page documents known limitations, architectural constraints, and operational considerations when using iam-audit. Understanding these limitations will help you interpret results correctly and plan remediations.

CloudTrail Regional Limitation

The script queries CloudTrail events only from us-east-1.

Why This Matters

IAM is a global service, but CloudTrail events are regional. All IAM API calls (regardless of where they originate) generate CloudTrail events in us-east-1 by default. Affected Code:
ct_client = boto3.client(
    'cloudtrail',
    region_name='us-east-1',  # Hardcoded
    aws_access_key_id=credentials['AccessKeyId'],
    aws_secret_access_key=credentials['SecretAccessKey'],
    aws_session_token=credentials['SessionToken']
)
Source: iam_audit.py:94-100

What This Means

Captured: All IAM events (CreateUser, DeleteAccessKey, etc.) across all regions Not Captured: Regional service events in other regions (e.g., EC2, S3 in eu-west-1)

Workaround

If you need multi-region CloudTrail coverage:
  1. Create an organization-wide CloudTrail trail
  2. Query events from a centralized S3 bucket using Athena
  3. Modify the script to query multiple regions:
regions = ['us-east-1', 'eu-west-1', 'ap-southeast-1']
for region in regions:
    ct_client = boto3.client('cloudtrail', region_name=region, ...)
    # Query events per region

Accounts Without Audit Role

Accounts where the specified role is not deployed will be skipped with a console error.

Expected Behavior

When the script cannot assume the audit role:
Error en cuenta ProductionAccount: An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::111111111111:user/auditor is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::222222222222:role/IAMAuditRole
The audit continues with remaining accounts. This is by design.

Why This Happens

  • Role not deployed in the target account
  • Trust policy missing or misconfigured
  • Role name mismatch (typo in --role argument)
  • Account is suspended or inaccessible

How to Detect

A skipped account is itself a security finding. It indicates gaps in your organization’s security baseline.
To detect skipped accounts:
  1. Check console output for Error en cuenta messages
  2. Compare the number of accounts in Organizations vs. accounts in the CSV report
  3. Use AWS CLI to verify role deployment:
aws iam get-role --role-name IAMAuditRole --profile <account-profile>

Resolution

Option 1: Deploy the audit role using CloudFormation StackSets
aws cloudformation create-stack-set \
  --stack-set-name IAMAuditRole \
  --template-body file://audit-role.yaml \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true
Option 2: Use AWS Control Tower’s AWSControlTowerExecution role (if available)
python iam_audit.py --profile mgmt --role AWSControlTowerExecution

Root Account Keys Not Audited

The script does not audit root account access keys.

Why

The IAM API (list_users) only returns IAM users, not the root account. Root account credentials are managed separately.

Impact

You will not see root access keys in the audit report, even if they exist.

Workaround

Manually check root credentials:
  1. Log into each account as root
  2. Go to My Security Credentials
  3. Check the Access keys section
Or use AWS Config with the iam-root-access-key-check managed rule:
aws configservice put-config-rule \
  --config-rule file://root-key-check.json

Future Work

This limitation is documented in the roadmap:
  • Detección de root account keys
Source: README.md:162

Pagination Considerations

IAM User Pagination

The script uses pagination correctly for list_users and list_accounts:
paginator = org_client.get_paginator('list_accounts')
for page in paginator.paginate():
    # Process accounts
Safe for organizations with any number of accounts or users.

CloudTrail Pagination

CloudTrail lookup_events also uses pagination:
paginator = ct_client.get_paginator('lookup_events')
for page in paginator.paginate(...):
    # Process events
Safe for large event volumes.
No manual pagination limits are needed. Boto3 handles pagination automatically.

API Rate Limits and Throttling

IAM API Throttling

The script makes multiple IAM API calls per user:
  • list_access_keys
  • list_mfa_devices
  • get_login_profile
  • get_access_key_last_used
In accounts with hundreds of IAM users, you may hit IAM rate limits:
An error occurred (Throttling) when calling the GetAccessKeyLastUsed operation: Rate exceeded

Mitigation Strategies

1. Add exponential backoff (recommended):
from botocore.config import Config

config = Config(
    retries={
        'max_attempts': 10,
        'mode': 'adaptive'
    }
)

iam_client = boto3.client('iam', config=config, ...)
2. Add sleep between accounts:
import time

for account in accounts:
    findings = get_iam_users_with_keys(...)
    time.sleep(2)  # 2-second delay
3. Run during off-peak hours when API load is lower.

CloudTrail Rate Limits

lookup_events is rate-limited to 2 requests per second per account. With the current implementation:
  • 5 event types × number of accounts = total requests
  • Pagination adds additional requests
In organizations with 50+ accounts, expect throttling errors. The script will continue after errors.

Console Output Language

Error messages and console output are in Spanish:
print(f"Auditando cuenta: {account['name']} ({account['id']})")
print(f"Error en cuenta {account['name']}: {e}")

CSV Field Values

Some CSV fields contain Spanish values:
  • password_status: "Configurada" or "No configurada"
  • password_last_used: "Nunca"
  • last_used_date: "Nunca utilizada"

Impact

If you’re building automation on top of the CSV output, use these exact strings in your logic:
if finding['password_status'] == 'Configurada':
    # User has console access

Workaround

To localize output, modify these strings in iam_audit.py:
# Line 56
password_status = 'Configured'  # Instead of 'Configurada'

# Line 58
password_status = 'Not configured'  # Instead of 'No configurada'

# Line 66
last_used_date = str(last_used.get('LastUsedDate', 'Never used'))

Hardcoded Date Range

The CloudTrail start date is hardcoded in the script.
Current implementation:
start_date = datetime(2026, 2, 18)  # Fixed date
end_date = datetime.now()
Source: iam_audit.py:137-138

Impact

CloudTrail events are only retrieved from February 18, 2026 onwards. Events before this date are not included in the report.

Workaround

Modify the script to accept date range as CLI arguments:
parser.add_argument('--start-date', type=str, help='Start date (YYYY-MM-DD)')
parser.add_argument('--days', type=int, default=30, help='Days to look back')

args = parse_args()
if args.start_date:
    start_date = datetime.strptime(args.start_date, '%Y-%m-%d')
else:
    start_date = datetime.now() - timedelta(days=args.days)

IAM Permissions Scope

The audit role requires read-only IAM permissions:
{
  "Effect": "Allow",
  "Action": [
    "iam:ListUsers",
    "iam:ListAccessKeys",
    "iam:GetAccessKeyLastUsed",
    "iam:ListMFADevices",
    "iam:GetLoginProfile",
    "cloudtrail:LookupEvents"
  ],
  "Resource": "*"
}

What’s NOT Included

❌ IAM policies attached to users ❌ Group memberships ❌ Inline policies ❌ Permission boundaries ❌ IAM roles (only users are audited)

Why This Matters

The audit focuses only on long-term credentials (access keys and passwords). It does not evaluate what permissions those credentials have.
To audit permissions, consider using AWS IAM Access Analyzer or AWS Config’s iam-policy-no-statements-with-admin-access rule.

No Real-Time Monitoring

The script is a point-in-time audit, not a continuous monitoring solution.

Limitations

  • Must be run manually or scheduled (e.g., via cron, Lambda)
  • No alerting for new access keys created after the audit
  • No integration with AWS Security Hub or GuardDuty

Future Roadmap

Planned features to address this:
  • Integración con AWS Security Hub (custom findings)
  • Alertas por Slack / email para keys de alto riesgo
  • Ejecución programada vía Lambda
Source: README.md:164-167

CSV File Overwrites

Filename format:
iam_audit_report_YYYYMMDD_HHMMSS.csv
cloudtrail_events_YYYYMMDD_HHMMSS.csv
✅ Timestamps prevent accidental overwrites when running the script multiple times in the same day. ❌ No automatic cleanup of old reports. CSV files accumulate in the working directory.

Cleanup Recommendation

# Archive reports older than 30 days
find . -name "iam_audit_report_*.csv" -mtime +30 -exec mv {} ./archive/ \;
find . -name "cloudtrail_events_*.csv" -mtime +30 -exec mv {} ./archive/ \;

Empty Report Scenarios

No Findings

If the script outputs:
No se encontraron Access Keys.
Possible causes:
  1. Good news: No IAM users have access keys (ideal state)
  2. ⚠️ All accounts were skipped due to role assumption errors
  3. ⚠️ The management account profile has no access to Organizations
Verify:
aws organizations list-accounts --profile <your-profile>

No CloudTrail Events

If Total de eventos CloudTrail encontrados: 0: Possible causes:
  1. No IAM events occurred in the specified date range
  2. CloudTrail is not enabled in the accounts
  3. CloudTrail lookup permissions are missing
  4. Events are older than CloudTrail retention (90 days by default)
Verify:
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateUser --profile <account-profile>

Summary

LimitationSeverityWorkaround Available
CloudTrail only in us-east-1LowYes (multi-region query)
Accounts without role skippedMediumYes (deploy role via StackSets)
Root keys not auditedHighManual check or AWS Config
API rate limitsMediumYes (exponential backoff)
Hardcoded date rangeLowYes (CLI arguments)
Spanish console outputLowYes (modify strings)
No real-time monitoringMediumPlanned (Lambda/Security Hub)
No permission analysisLowUse IAM Access Analyzer
These limitations are documented as of the current version. Check the GitHub repository for updates and community workarounds.

Build docs developers (and LLMs) love