Overview
This guide provides actionable remediation workflows for common IAM security findings identified by the audit tool. Each workflow includes AWS CLI commands, risk considerations, and rollback procedures.
Always test remediation workflows in non-production environments first. Communicate changes to affected users before taking action.
Before executing any remediation:
Identify the Owner
Determine who owns the IAM user or access key: aws iam list-user-tags --user-name USERNAME --profile ACCOUNT_PROFILE
Look for Owner, Team, or Email tags.
Assess Impact
Check if the credential is actively used:
Review last_used_date in the audit report
Check CloudTrail for recent API activity
Document the Change
Create a ticket or log entry with:
Finding details (username, key ID, account)
Planned remediation action
Expected completion date
Workflow 1: Enforce MFA for Console Users
Problem
Users with console access but no MFA protection:
password_status: Configurada
mfa_status: None
Identify Affected Users
Filter the audit report: awk -F ',' '$4=="Configurada" && $11=="None" {print $2","$3}' iam_audit_report_ * .csv | sort -u
Output: account_name,username
Require MFA via IAM Policy
Create a policy that denies all actions except MFA setup until MFA is enabled: {
"Version" : "2012-10-17" ,
"Statement" : [
{
"Sid" : "DenyAllExceptMFASetup" ,
"Effect" : "Deny" ,
"NotAction" : [
"iam:CreateVirtualMFADevice" ,
"iam:EnableMFADevice" ,
"iam:GetUser" ,
"iam:ListMFADevices" ,
"iam:ListVirtualMFADevices" ,
"iam:ResyncMFADevice" ,
"sts:GetSessionToken"
],
"Resource" : "*" ,
"Condition" : {
"BoolIfExists" : {
"aws:MultiFactorAuthPresent" : "false"
}
}
}
]
}
Attach Policy to Users
Apply the policy: aws iam put-user-policy \
--user-name USERNAME \
--policy-name RequireMFAPolicy \
--policy-document file://require-mfa.json \
--profile ACCOUNT_PROFILE
Verify Enforcement
User attempts to access AWS Console → Forced to set up MFA before proceeding.
Remove Policy After MFA Setup
Once MFA is enabled: aws iam delete-user-policy \
--user-name USERNAME \
--policy-name RequireMFAPolicy \
--profile ACCOUNT_PROFILE
This approach forces users to set up MFA immediately. Provide clear instructions to avoid support tickets.
Alternative: Organization-Level MFA Enforcement
For AWS Organizations, use a Service Control Policy (SCP):
{
"Version" : "2012-10-17" ,
"Statement" : [
{
"Sid" : "DenyConsoleAccessWithoutMFA" ,
"Effect" : "Deny" ,
"Action" : "*" ,
"Resource" : "*" ,
"Condition" : {
"BoolIfExists" : {
"aws:MultiFactorAuthPresent" : "false"
},
"StringNotEquals" : {
"aws:PrincipalType" : "AssumedRole"
}
}
}
]
}
Apply to OUs containing production accounts.
Workflow 2: Disable Old Access Keys Safely
Problem
Access keys older than 90 days (or organizational threshold):
created_date exceeds age policy
status: Active
Identify Old Keys
Extract keys older than 90 days: awk -F ',' 'NR>1 {print $1","$3","$6","$8}' iam_audit_report_ * .csv | \
awk -F ',' '{split($4,a," "); if ((systime()-mktime(gensub(/-/," ","g",a[1])" "a[2]))/86400 > 90) print}'
Check Recent Usage
For each key, verify last_used_date in the report. If used within 7 days, coordinate with the owner.
Deactivate (Don't Delete) First
IMPORTANT: Deactivate instead of deleting to allow quick rollback:aws iam update-access-key \
--user-name USERNAME \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive \
--profile ACCOUNT_PROFILE
Monitor for Failures
Watch CloudTrail for authentication failures over the next 24-48 hours: aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAIOSFODNN7EXAMPLE \
--start-time $( date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%S ) \
--profile ACCOUNT_PROFILE
Delete After Confirmation
If no issues reported after 48 hours: aws iam delete-access-key \
--user-name USERNAME \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--profile ACCOUNT_PROFILE
The two-step process (deactivate → delete) gives you a safety net. Reactivating a key is instant; recovering a deleted key is impossible.
Rollback Procedure
If an application breaks after deactivation:
aws iam update-access-key \
--user-name USERNAME \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Active \
--profile ACCOUNT_PROFILE
Then work with the application owner to migrate to IAM roles.
Workflow 3: Migrate from Long-Term to Temporary Credentials
Problem
Applications using long-term access keys instead of temporary credentials:
Keys embedded in application code or environment variables
Services that could use IAM roles but don’t
Create IAM Role for EC2
aws iam create-role \
--role-name MyAppEC2Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' \
--profile ACCOUNT_PROFILE
Attach Permissions
Grant the same permissions the access key had: aws iam attach-role-policy \
--role-name MyAppEC2Role \
--policy-arn arn:aws:iam::aws:policy/SERVICE_POLICY \
--profile ACCOUNT_PROFILE
Create Instance Profile
aws iam create-instance-profile \
--instance-profile-name MyAppEC2Role \
--profile ACCOUNT_PROFILE
aws iam add-role-to-instance-profile \
--instance-profile-name MyAppEC2Role \
--role-name MyAppEC2Role \
--profile ACCOUNT_PROFILE
Attach to EC2 Instance
aws ec2 associate-iam-instance-profile \
--instance-id i-1234567890abcdef0 \
--iam-instance-profile Name=MyAppEC2Role \
--profile ACCOUNT_PROFILE
Update Application Code
Remove hardcoded credentials. AWS SDKs automatically use instance profile credentials: # Before (using access keys)
import boto3
s3 = boto3.client( 's3' ,
aws_access_key_id = 'AKIAI...' ,
aws_secret_access_key = '...' )
# After (using IAM role)
import boto3
s3 = boto3.client( 's3' ) # Automatically uses instance role
Test and Deactivate Key
Verify the application works with the IAM role, then deactivate the old access key.
Common Migration Scenarios
Problem: Lambda using access keys stored in environment variables.Solution: Attach an execution role with required permissions:aws lambda update-function-configuration \
--function-name MyFunction \
--role arn:aws:iam::ACCOUNT_ID:role/LambdaExecutionRole \
--profile ACCOUNT_PROFILE
Remove AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.
Problem: ECS containers with access keys in task definitions.Solution: Use task IAM roles:{
"family" : "my-task" ,
"taskRoleArn" : "arn:aws:iam::ACCOUNT_ID:role/MyTaskRole" ,
"containerDefinitions" : [ ... ]
}
Register updated task definition and redeploy services.
Problem: Access keys used to access resources in another account.Solution: Use IAM role assumption:aws sts assume-role \
--role-arn arn:aws:iam::TARGET_ACCOUNT:role/CrossAccountRole \
--role-session-name MySession \
--profile SOURCE_ACCOUNT_PROFILE
Update application to assume role instead of using static keys.
Problem: Build pipelines using long-term keys.Solution: Use OIDC federation with GitHub Actions, GitLab, or other providers:# GitHub Actions example
- name : Configure AWS Credentials
uses : aws-actions/configure-aws-credentials@v2
with :
role-to-assume : arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
aws-region : us-east-1
No access keys required.
Workflow 4: Delete Unused IAM Users
Problem
Users with no recent activity (zombie accounts):
password_last_used is older than 90 days (or never used)
last_used_date shows no key usage in 90 days
Verify User is Inactive
Cross-reference with HR/IT to confirm user has left the organization or no longer needs access.
Audit User Permissions
Check what the user had access to: aws iam list-attached-user-policies --user-name USERNAME --profile ACCOUNT_PROFILE
aws iam list-user-policies --user-name USERNAME --profile ACCOUNT_PROFILE
aws iam list-groups-for-user --user-name USERNAME --profile ACCOUNT_PROFILE
Delete Access Keys First
aws iam list-access-keys --user-name USERNAME --profile ACCOUNT_PROFILE
aws iam delete-access-key --user-name USERNAME --access-key-id AKIAI... --profile ACCOUNT_PROFILE
Delete Login Profile
Remove console access: aws iam delete-login-profile --user-name USERNAME --profile ACCOUNT_PROFILE
Remove from Groups
aws iam remove-user-from-group --user-name USERNAME --group-name GROUPNAME --profile ACCOUNT_PROFILE
Detach Policies
aws iam detach-user-policy --user-name USERNAME --policy-arn POLICY_ARN --profile ACCOUNT_PROFILE
Delete Inline Policies
aws iam delete-user-policy --user-name USERNAME --policy-name POLICY_NAME --profile ACCOUNT_PROFILE
Delete the User
aws iam delete-user --user-name USERNAME --profile ACCOUNT_PROFILE
IAM user deletion is permanent. Ensure you have proper approval before executing this workflow.
Workflow 5: Rotate Access Keys Without Downtime
Problem
Keys need rotation but application requires 24/7 availability.
Create Second Access Key
AWS allows two keys per user: aws iam create-access-key --user-name USERNAME --profile ACCOUNT_PROFILE
Save the new AccessKeyId and SecretAccessKey.
Update Application Configuration
Deploy the new key to your application (environment variables, secrets manager, etc.).
Test with New Key
Verify the application works with the new credentials.
Deactivate Old Key
aws iam update-access-key \
--user-name USERNAME \
--access-key-id OLD_KEY_ID \
--status Inactive \
--profile ACCOUNT_PROFILE
Monitor for Errors
Wait 48 hours. If no issues arise, delete the old key: aws iam delete-access-key \
--user-name USERNAME \
--access-key-id OLD_KEY_ID \
--profile ACCOUNT_PROFILE
This workflow eliminates downtime by ensuring the new key works before removing the old one.
After executing remediation, confirm the actions appear in CloudTrail:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=DeleteAccessKey \
--start-time $( date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%S ) \
--profile ACCOUNT_PROFILE \
--query 'Events[*].CloudTrailEvent' \
--output text | jq .
Track Progress Over Time
Re-run IAM Audit monthly and compare CloudTrail reports:
# Count DeleteAccessKey events this month
grep "DeleteAccessKey" cloudtrail_events_ * .csv | wc -l
# Count CreateAccessKey events (should be decreasing)
grep "CreateAccessKey" cloudtrail_events_ * .csv | wc -l
If CreateAccessKey events are increasing, investigate why new long-term credentials are being created.
Communication Templates
Template 1: MFA Enforcement Notification
Subject: ACTION REQUIRED: Enable MFA for AWS Console Access
Hi [Username],
Our security audit identified that your AWS IAM user account has console access without Multi-Factor Authentication (MFA) enabled.
**Action Required:**
Enable MFA within 7 days to maintain console access. After this period, access will be restricted until MFA is configured.
**How to Enable MFA:**
1. Log in to AWS Console: https://console.aws.amazon.com/
2. Navigate to IAM → Users → [Your Username]
3. Security Credentials tab → Assign MFA device
4. Follow the setup wizard (recommended: virtual MFA via mobile app)
**Why This Matters:**
MFA protects against password compromise and is required by our security policy.
Questions? Reply to this email or contact [Security Team Contact].
Thank you,
[Your Security Team]
Template 2: Access Key Deactivation Notice
Subject: AWS Access Key Scheduled for Deactivation
Hi [Username],
Our IAM audit identified an access key associated with your account that was created on [DATE] — over [X] days ago.
**Access Key ID:** AKIAI***EXAMPLE (last 7 characters hidden)
**Last Used:** [DATE]
**Service:** [SERVICE_NAME]
**Action:**
This key will be deactivated on [DATE + 7 DAYS]. If you still require this key:
1. Reply to this email with justification
2. We'll work with you to migrate to temporary credentials (IAM roles)
**No Action Required If:**
- You no longer use this key
- You've already migrated to IAM roles
Questions? Contact [Security Team Contact].
Thank you,
[Your Security Team]
Template 3: User Deletion Notice
Subject: AWS IAM User Account Scheduled for Deletion
Hi [Manager/Team Lead],
Our IAM audit identified an inactive user account:
- **Username:** [USERNAME]
- **Account:** [ACCOUNT_NAME]
- **Last Console Login:** [DATE or "Never"]
- **Last Access Key Usage:** [DATE or "Never"]
**Reason for Deletion:**
The account has been inactive for over 90 days and appears to belong to a former team member.
**Action:**
This account will be deleted on [DATE + 14 DAYS]. If this account is still needed, please reply immediately with:
- Current owner's name and contact
- Business justification for continued access
**No Response = Account Deleted**
Thank you,
[Your Security Team]
Automation Opportunities
Scheduled Audits with Lambda
Run IAM Audit monthly via Lambda:
import boto3
import subprocess
from datetime import datetime
def lambda_handler ( event , context ):
# Clone your iam-audit repo or package it with Lambda
# Run the audit
result = subprocess.run(
[ 'python' , 'iam_audit.py' , '--profile' , 'mgmt' , '--role' , 'AuditRole' ],
capture_output = True
)
# Upload CSV reports to S3
s3 = boto3.client( 's3' )
timestamp = datetime.now().strftime( '%Y%m %d _%H%M%S' )
s3.upload_file(
f 'iam_audit_report_ { timestamp } .csv' ,
'my-audit-bucket' ,
f 'reports/iam_audit_report_ { timestamp } .csv'
)
return { 'statusCode' : 200 , 'body' : 'Audit complete' }
Schedule with EventBridge: cron(0 9 1 * ? *) (monthly on the 1st at 9 AM UTC)
Automated Key Rotation
Key Age Alert via CloudWatch
Create a Lambda function that:
Reads the latest audit CSV from S3
Identifies keys older than 90 days
Sends SNS notifications to key owners
Optionally auto-deactivates keys older than 180 days
Trigger weekly via EventBridge.
Security Hub Custom Findings
Push Findings to Security Hub
import boto3
import csv
from datetime import datetime
def send_to_security_hub ( findings ):
securityhub = boto3.client( 'securityhub' )
for finding in findings:
if finding[ 'mfa_status' ] == 'None' and finding[ 'password_status' ] == 'Configurada' :
securityhub.batch_import_findings(
Findings = [{
'SchemaVersion' : '2018-10-08' ,
'Id' : f "iam-audit/ { finding[ 'account_id' ] } / { finding[ 'username' ] } " ,
'ProductArn' : f "arn:aws:securityhub: { region } : { account_id } :product/ { account_id } /default" ,
'GeneratorId' : 'iam-audit-tool' ,
'AwsAccountId' : finding[ 'account_id' ],
'Types' : [ 'Software and Configuration Checks/AWS Security Best Practices' ],
'CreatedAt' : datetime.now().isoformat() + 'Z' ,
'UpdatedAt' : datetime.now().isoformat() + 'Z' ,
'Severity' : { 'Label' : 'HIGH' },
'Title' : f "IAM user { finding[ 'username' ] } has console access without MFA" ,
'Description' : f "User { finding[ 'username' ] } can access AWS Console without multi-factor authentication." ,
'Resources' : [{
'Type' : 'AwsIamUser' ,
'Id' : f "arn:aws:iam:: { finding[ 'account_id' ] } :user/ { finding[ 'username' ] } " ,
'Region' : region
}]
}]
)
Key Metrics to Track
Metric Target Measurement Method MFA Coverage 100% for console users (Users with MFA / Total console users) × 100Key Age No keys older than 90 days Count keys where created_date exceeds threshold Unused Keys Zero keys never used Count keys with last_used_date: Nunca utilizada User Cleanup Rate All inactive users removed within 30 days Track DeleteUser events in CloudTrail Key Rotation Velocity Decrease in average key age month-over-month Calculate median key age from audit reports
Create a simple tracking spreadsheet:
Month | Total Keys | Keys >90d | Keys >1yr | MFA Coverage | Inactive Users
2026-01 | 245 | 87 | 12 | 65% | 23
2026-02 | 230 | 62 | 8 | 78% | 15
2026-03 | 198 | 34 | 2 | 92% | 6
Visualize trends to show security improvements to leadership.
Troubleshooting Common Issues
Error: User has multiple policies, can't delete
Problem: IAM user deletion fails because policies are still attached.Solution: Run this script to clean up all attachments:#!/bin/bash
USERNAME = $1
PROFILE = $2
# Detach managed policies
aws iam list-attached-user-policies --user-name $USERNAME --profile $PROFILE \
--query 'AttachedPolicies[*].PolicyArn' --output text | \
xargs -n1 aws iam detach-user-policy --user-name $USERNAME --profile $PROFILE --policy-arn
# Delete inline policies
aws iam list-user-policies --user-name $USERNAME --profile $PROFILE \
--query 'PolicyNames[*]' --output text | \
xargs -n1 aws iam delete-user-policy --user-name $USERNAME --profile $PROFILE --policy-name
# Remove from groups
aws iam list-groups-for-user --user-name $USERNAME --profile $PROFILE \
--query 'Groups[*].GroupName' --output text | \
xargs -n1 aws iam remove-user-from-group --user-name $USERNAME --profile $PROFILE --group-name
# Delete access keys
aws iam list-access-keys --user-name $USERNAME --profile $PROFILE \
--query 'AccessKeyMetadata[*].AccessKeyId' --output text | \
xargs -n1 aws iam delete-access-key --user-name $USERNAME --profile $PROFILE --access-key-id
# Delete login profile
aws iam delete-login-profile --user-name $USERNAME --profile $PROFILE 2> /dev/null
# Finally delete user
aws iam delete-user --user-name $USERNAME --profile $PROFILE
Error: Access denied when assuming role
Problem: Can’t assume audit role in child accounts.Solution: Verify:
Trust relationship in child account role allows management account:
{
"Version" : "2012-10-17" ,
"Statement" : [{
"Effect" : "Allow" ,
"Principal" : { "AWS" : "arn:aws:iam::MGMT_ACCOUNT_ID:root" },
"Action" : "sts:AssumeRole"
}]
}
Management account has sts:AssumeRole permission for that role ARN.
Application breaks after key deactivation
Problem: Critical service stops working.Solution:
Immediately reactivate the key (see Workflow 2 Rollback )
Identify the application using CloudTrail
Schedule a maintenance window to migrate to IAM roles
Use Workflow 3 to implement temporary credentials
Next Steps
Interpreting Results Understand how to analyze audit findings
Security Maturity Model Track your AWS SMM progress
AWS IAM Best Practices Official AWS security recommendations
Temporary Credentials Guide Deep dive into AWS STS and temporary credentials