7 AWS IAM Security Mistakes Every Developer Makes
AWS IAM: The Most Important (and Most Misconfigured) Service
If your AWS IAM is misconfigured, nothing else matters. Not your WAF, not your VPC, not your encryption. An overprivileged IAM role is the master key to your entire cloud environment.
Fact: According to Datadog's 2024 State of Cloud Security report, 45% of organizations have at least one IAM user with long-lived credentials that haven't been rotated in over 90 days.
Mistake #1: Using Root Account for Daily Tasks
The AWS root account has unrestricted access to every service and resource. It cannot be limited by IAM policies.
❌ Common Anti-Pattern
Developer: "I just use the root account, it's easier."
Result: One compromised credential = total account takeover
✅ Fix
# 1. Enable MFA on root account
aws iam enable-mfa-device --user-name root \
--serial-number arn:aws:iam::mfa/root-account-mfa-device \
--authentication-code1 123456 --authentication-code2 789012
# 2. Create admin user with limited permissions
aws iam create-user --user-name admin-user
aws iam attach-user-policy --user-name admin-user \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# 3. Store root credentials in a safe, use only for:
# - Closing account
# - Changing root email
# - Enabling MFA
# - Restoring IAM permissions
Mistake #2: Wildcard Permissions (*)
❌ The "Just Make It Work" Policy
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}
This grants every possible permission — S3, EC2, IAM, Lambda, billing, everything.
✅ Fix: Least-Privilege Policy
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}, {
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/my-app-table"
}]
}
Generate Least-Privilege Policies
# Use IAM Access Analyzer to generate policy from actual usage
aws accessanalyzer start-policy-generation \
--policy-generation-details '{"principalArn":"arn:aws:iam::role/my-role"}'
# Or use iamlive to capture needed permissions
iamlive --mode proxy --force-wildcard-resource
# ... run your application ...
# iamlive outputs the minimum required policy
Mistake #3: Long-Lived Access Keys
❌ Keys That Never Expire
# Check for old access keys
aws iam list-access-keys --user-name developer
# Created: 2022-03-15 — over 2 years old!
✅ Fix: Use IAM Roles + Temporary Credentials
# Terraform — EC2 instance with IAM role (no access keys needed)
resource "aws_iam_role" "app_role" {
name = "app-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
resource "aws_iam_instance_profile" "app_profile" {
name = "app-profile"
role = aws_iam_role.app_role.name
}
resource "aws_instance" "app" {
ami = "ami-12345678"
instance_type = "t3.micro"
iam_instance_profile = aws_iam_instance_profile.app_profile.name
# No access keys needed — uses instance metadata for credentials
}
# For CI/CD — use OIDC instead of access keys
# GitHub Actions example (no static credentials)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::role/github-actions-role
aws-region: us-east-1
Mistake #4: Not Using Permission Boundaries
Without boundaries, anyone with iam:CreateRole can create an admin role.
✅ Fix: Set Permission Boundaries
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:*",
"dynamodb:*",
"lambda:*",
"logs:*"
],
"Resource": "*"
}, {
"Sid": "DenyIAMChanges",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"organizations:*"
],
"Resource": "*"
}]
}
Mistake #5: Cross-Account Trust Too Broad
❌ Trusting an Entire Account
{
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "sts:AssumeRole"
}]
}
Any role in account 123456789012 can assume this role — including compromised roles.
✅ Fix: Trust Specific Roles
{
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/specific-deploy-role"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id-12345"
}
}
}]
}
Mistake #6: No SCPs (Service Control Policies)
SCPs provide guardrails at the organization level. Without them, any account can do anything.
✅ Fix: Key SCPs Every Org Needs
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDisablingCloudTrail",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail"
],
"Resource": "*"
},
{
"Sid": "DenyLeavingOrg",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
},
{
"Sid": "RequireIMDSv2",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
]
}
Mistake #7: No CloudTrail Monitoring for IAM Events
✅ Fix: Monitor Critical IAM Events
# CloudWatch alarm for root account usage
aws cloudwatch put-metric-alarm \
--alarm-name "RootAccountUsage" \
--metric-name "RootAccountUsageCount" \
--namespace "CloudTrailMetrics" \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--period 300 \
--threshold 1 \
--statistic Sum \
--alarm-actions arn:aws:sns:us-east-1:*:security-alerts
Key IAM Events to Monitor
| Event | Why |
|---|---|
| ConsoleLogin (root) | Root should never be used |
| CreateAccessKey | Static credentials being created |
| AttachUserPolicy / AttachRolePolicy | Privilege escalation |
| CreateUser / CreateRole | New principals |
| PutBucketPolicy | S3 access changes |
| AuthorizeSecurityGroupIngress | Network access changes |
IAM Security Audit Script
#!/bin/bash
echo "=== AWS IAM Security Quick Audit ==="
echo "\n--- Users with console access ---"
aws iam list-users --query 'Users[*].UserName' --output table
echo "\n--- Users with access keys ---"
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
keys=$(aws iam list-access-keys --user-name $user --query 'AccessKeyMetadata[*].[Status,CreateDate]' --output text)
if [ -n "$keys" ]; then
echo "$user: $keys"
fi
done
echo "\n--- Policies with admin access ---"
aws iam list-policies --only-attached --query 'Policies[?PolicyName==`AdministratorAccess`]'
echo "\n--- MFA status ---"
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
mfa=$(aws iam list-mfa-devices --user-name $user --query 'MFADevices' --output text)
if [ -z "$mfa" ]; then
echo "NO MFA: $user"
fi
done
Need an AWS Security Audit?
We review IAM policies, VPC configurations, S3 bucket policies, and CloudTrail setups. Request a free consultation →
Published by SecureCodeReviews.com — helping teams lock down their AWS infrastructure.
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Cloud Security Guide: AWS, Azure & GCP Misconfigurations 2025
Master cloud security with comprehensive guides on S3 bucket security, IAM policies, secrets management, and real breach case studies.
Cloud Security in 2025: Comprehensive Guide for AWS, Azure & GCP
Deep-dive into cloud security best practices across all three major providers. Covers IAM, network security, data encryption, compliance, and real-world misconfigurations that led to breaches.
How to Secure AI Agents: Identity & Access Management for Agentic AI
Machine identities now outnumber human identities 45:1. Learn how to implement IAM for AI agents — authentication, authorization, credential management, and delegation chains in multi-agent systems.