Cloud Security
AWS
IAM
Cloud Security
Terraform

7 AWS IAM Security Mistakes Every Developer Makes

SecureCodeReviews Team
January 15, 2025
15 min read
Share

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

EventWhy
ConsoleLogin (root)Root should never be used
CreateAccessKeyStatic credentials being created
AttachUserPolicy / AttachRolePolicyPrivilege escalation
CreateUser / CreateRoleNew principals
PutBucketPolicyS3 access changes
AuthorizeSecurityGroupIngressNetwork 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