DEV Community

MD Nur Ahmed
MD Nur Ahmed

Posted on

Mastering AWS IAM: A Complete Guide with Hands-On Scenarios

Mastering AWS IAM: A Complete Guide with Hands-On Scenarios

AWS Identity and Access Management (IAM) is the backbone of security in AWS. Whether you're deploying your first Lambda function or architecting a multi-account enterprise solution, understanding IAM is crucial. In this comprehensive guide, I'll walk you through different types of IAM identities, policies, and real-world scenarios with practical Terraform examples.

What is AWS IAM?

AWS IAM is a web service that helps you securely control access to AWS resources. It enables you to manage authentication (who can sign in) and authorization (what actions they can perform) across your AWS infrastructure.

Think of IAM as the security guard of your AWS account. It decides:

  • Who can access your resources (authentication)
  • What they can do with those resources (authorization)
  • When and where they can access them (conditions)

Core IAM Components

1. IAM Users

Individual identities with long-term credentials. Best suited for:

  • Human users who need console or CLI access
  • Applications that require permanent credentials (though roles are preferred)
  • Service accounts for third-party integrations

2. IAM Groups

Collections of users with similar permissions. Groups help you:

  • Manage permissions at scale
  • Apply consistent policies across teams
  • Simplify user onboarding and offboarding

3. IAM Roles

Temporary identities that can be assumed by users, applications, or AWS services. Roles are perfect for:

  • AWS services (Lambda, EC2, ECS)
  • Cross-account access
  • Federated users
  • Applications running on AWS

4. IAM Policies

JSON documents that define permissions. Types include:

  • Identity-based policies: Attached to users, groups, or roles
  • Resource-based policies: Attached to resources like S3 buckets
  • Permission boundaries: Set maximum permissions
  • Service Control Policies (SCPs): Applied at the AWS Organizations level

IAM Best Practices

Before diving into scenarios, here are critical best practices:

  1. Follow the Principle of Least Privilege: Grant only the permissions required to perform a task
  2. Use Roles Instead of Users for Applications: Roles provide temporary credentials that rotate automatically
  3. Enable MFA for Privileged Users: Add an extra layer of security
  4. Regularly Rotate Credentials: Change passwords and access keys periodically
  5. Use Policy Conditions: Add constraints like IP address ranges or time windows
  6. Monitor with CloudTrail: Keep audit logs of all IAM actions
  7. Avoid Using Root Account: Create IAM users for daily operations

Real-World IAM Scenarios

Let me walk you through practical scenarios that you'll encounter in production environments. All the code examples are available in my AWS IAM Hands-On Repository.

Scenario 1: Lambda Function with S3 Access

Use Case: You have a Lambda function that needs to read from an S3 bucket and write logs to CloudWatch.

Solution: Create an IAM role with the necessary permissions and attach it to the Lambda function.

# IAM Role for Lambda
resource "aws_iam_role" "lambda_role" {
  name = "lambda-s3-access-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

# Policy for S3 access
resource "aws_iam_policy" "lambda_s3_policy" {
  name = "lambda-s3-access-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject"
        ]
        Resource = "arn:aws:s3:::my-bucket/*"
      },
      {
        Effect = "Allow"
        Action = "s3:ListBucket"
        Resource = "arn:aws:s3:::my-bucket"
      }
    ]
  })
}

# Attach policy to role
resource "aws_iam_role_policy_attachment" "lambda_s3_attach" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.lambda_s3_policy.arn
}

# Attach CloudWatch Logs policy
resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
Enter fullscreen mode Exit fullscreen mode

Why This Works:

  • The Lambda service can assume this role
  • The role has specific S3 permissions (read and write to a specific bucket)
  • CloudWatch Logs permissions allow Lambda to write logs
  • No hard-coded credentials needed

Scenario 2: Cross-Account Access

Use Case: You have a CI/CD pipeline in Account A that needs to deploy resources to Account B.

Solution: Create a role in Account B that Account A can assume.

# In Account B - Create role that Account A can assume
resource "aws_iam_role" "cross_account_role" {
  name = "cross-account-deploy-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::ACCOUNT_A_ID:root"
        }
        Action = "sts:AssumeRole"
        Condition = {
          StringEquals = {
            "sts:ExternalId" = "unique-external-id-123"
          }
        }
      }
    ]
  })
}

# Deployment permissions
resource "aws_iam_policy" "deploy_policy" {
  name = "deployment-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:*",
          "s3:*",
          "lambda:*",
          "cloudformation:*"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "deploy_attach" {
  role       = aws_iam_role.cross_account_role.name
  policy_arn = aws_iam_policy.deploy_policy.arn
}
Enter fullscreen mode Exit fullscreen mode

Security Highlights:

  • External ID prevents the "confused deputy" problem
  • Account A must explicitly assume the role
  • Permissions are isolated to Account B
  • Can be easily audited via CloudTrail

Scenario 3: EC2 Instance Profile

Use Case: Your EC2 instances need to access AWS services without storing credentials in the application.

Solution: Use an instance profile with an IAM role.

# IAM Role for EC2
resource "aws_iam_role" "ec2_role" {
  name = "ec2-app-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Application permissions
resource "aws_iam_policy" "app_policy" {
  name = "ec2-app-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:Query"
        ]
        Resource = "arn:aws:dynamodb:us-east-1:*:table/my-table"
      },
      {
        Effect = "Allow"
        Action = [
          "sqs:SendMessage",
          "sqs:ReceiveMessage"
        ]
        Resource = "arn:aws:sqs:us-east-1:*:my-queue"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "app_attach" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = aws_iam_policy.app_policy.arn
}

# Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
  name = "ec2-app-profile"
  role = aws_iam_role.ec2_role.name
}

# Attach to EC2 instance
resource "aws_instance" "app_server" {
  ami                  = "ami-0c55b159cbfafe1f0"
  instance_type        = "t2.micro"
  iam_instance_profile = aws_iam_instance_profile.ec2_profile.name

  # Your other configurations...
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • No credential management in application code
  • Automatic credential rotation by AWS
  • Easy to audit and rotate permissions
  • Credentials never leave the AWS environment

Scenario 4: Least Privilege with Condition Keys

Use Case: Grant developers access to only their team's resources using tags.

Solution: Use condition keys in IAM policies.

resource "aws_iam_policy" "team_policy" {
  name = "team-based-access-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:StartInstances",
          "ec2:StopInstances",
          "ec2:TerminateInstances"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "ec2:ResourceTag/Team" = "$${aws:username}"
          }
        }
      },
      {
        Effect = "Allow"
        Action = [
          "ec2:CreateTags"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "ec2:CreateAction" = "RunInstances"
            "aws:RequestTag/Team" = "$${aws:username}"
          }
        }
      },
      {
        Effect = "Allow"
        Action = [
          "ec2:Describe*"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_group" "developers" {
  name = "developers"
}

resource "aws_iam_group_policy_attachment" "dev_attach" {
  group      = aws_iam_group.developers.name
  policy_arn = aws_iam_policy.team_policy.arn
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  • Developers can only manage instances tagged with their username
  • Forces tagging at creation time
  • Read-only access to all resources for visibility
  • Prevents accidental modification of other teams' resources

Advanced IAM Patterns

Permission Boundaries

Permission boundaries set the maximum permissions an entity can have, even if more permissive policies are attached.

resource "aws_iam_policy" "permission_boundary" {
  name = "developer-boundary"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:*",
          "dynamodb:*",
          "lambda:*"
        ]
        Resource = "*"
      },
      {
        Effect = "Deny"
        Action = [
          "iam:*",
          "organizations:*"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_user" "developer" {
  name                 = "junior-developer"
  permissions_boundary = aws_iam_policy.permission_boundary.arn
}
Enter fullscreen mode Exit fullscreen mode

Session Policies

Provide additional restrictions when assuming a role:

# Assume role with session policy
resource "aws_iam_role" "assumed_role" {
  name = "limited-session-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::123456789012:user/admin"
        }
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

Then when assuming with AWS CLI:

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/limited-session-role \
  --role-session-name my-session \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "s3:GetObject",
        "Resource": "arn:aws:s3:::specific-bucket/*"
      }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

IAM Policy Evaluation Logic

Understanding how AWS evaluates policies is crucial:

  1. By default, all requests are denied (implicit deny)
  2. Explicit allow in an identity-based or resource-based policy overrides the default
  3. Permission boundary restricts the maximum permissions
  4. Explicit deny overrides any allows
Explicit Deny → Deny
    ↓
Permission Boundary → Deny if not allowed
    ↓
Identity-based Policy → Allow
    ↓
Resource-based Policy → Allow
    ↓
Default → Deny
Enter fullscreen mode Exit fullscreen mode

Testing IAM Policies

Always test your policies before deploying to production:

IAM Policy Simulator

Use the AWS IAM Policy Simulator to test policies:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/my-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/file.txt
Enter fullscreen mode Exit fullscreen mode

Access Analyzer

Enable IAM Access Analyzer to:

  • Identify resources shared with external entities
  • Validate policies against AWS best practices
  • Get recommendations for unused access

Common IAM Pitfalls and Solutions

1. Overly Permissive Policies

Problem: Using "Resource": "*" and "Action": "*"

Solution: Be specific with resources and actions

{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-specific-bucket/*"
}
Enter fullscreen mode Exit fullscreen mode

2. Not Using Roles for Applications

Problem: Storing access keys in code

Solution: Always use IAM roles with instance profiles or task roles

3. Forgetting to Test

Problem: Deploying untested policies

Solution: Use IAM Policy Simulator and Access Analyzer

4. Ignoring CloudTrail Logs

Problem: No visibility into who did what

Solution: Enable CloudTrail and review logs regularly

Hands-On Practice

Want to try these scenarios yourself? Check out my AWS IAM Hands-On Repository which contains:

  • Scenario 1: Lambda with S3 and DynamoDB access
  • Scenario 2: Cross-account role assumption
  • Scenario 3: EC2 instance profiles with multiple service integrations
  • Scenario 4: Tag-based access control with conditions
  • Lambda Code: Sample functions demonstrating IAM in action

Each scenario includes:

  • Terraform infrastructure code
  • Step-by-step deployment instructions
  • Testing procedures
  • Cleanup scripts
# Clone the repository
git clone https://github.com/mdnurahmed/aws-iam-hands-ons.git

# Navigate to a scenario
cd aws-iam-hands-ons/scenario-1

# Initialize Terraform
terraform init

# Plan the deployment
terraform plan

# Apply the configuration
terraform apply
Enter fullscreen mode Exit fullscreen mode

IAM Security Checklist

Use this checklist to ensure your IAM setup is secure:

  • [ ] Root account MFA enabled
  • [ ] No access keys for root account
  • [ ] IAM users have MFA enabled
  • [ ] Access keys rotated regularly (90 days max)
  • [ ] Unused users and roles removed
  • [ ] CloudTrail enabled and logs analyzed
  • [ ] IAM Access Analyzer enabled
  • [ ] Password policy enforced (complexity, rotation)
  • [ ] Least privilege principle applied
  • [ ] Service roles used instead of user credentials
  • [ ] Cross-account access uses external IDs
  • [ ] Permission boundaries set for delegated admin
  • [ ] Regular IAM credential reports reviewed

Monitoring and Compliance

CloudTrail Integration

Monitor IAM activities:

resource "aws_cloudtrail" "iam_trail" {
  name                          = "iam-audit-trail"
  s3_bucket_name               = aws_s3_bucket.trail_bucket.id
  include_global_service_events = true
  is_multi_region_trail        = true

  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }
}
Enter fullscreen mode Exit fullscreen mode

AWS Config Rules

Set up compliance checks:

resource "aws_config_config_rule" "iam_password_policy" {
  name = "iam-password-policy-check"

  source {
    owner             = "AWS"
    source_identifier = "IAM_PASSWORD_POLICY"
  }

  depends_on = [aws_config_configuration_recorder.main]
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

IAM is fundamental to AWS security, and mastering it takes practice. The key principles to remember:

  1. Always use least privilege - grant only what's needed
  2. Prefer roles over users - for applications and services
  3. Test before deploying - use simulators and analyzers
  4. Monitor continuously - enable CloudTrail and Config
  5. Automate with IaC - use Terraform or CloudFormation

By following these patterns and practicing with the scenarios in the AWS IAM Hands-On Repository, you'll build a strong foundation in AWS security.

What IAM challenges have you faced? Share your experiences in the comments below!


Additional Resources


If you found this guide helpful, give it a ❤️ and follow me for more AWS and DevOps content!

Top comments (0)