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:
- Follow the Principle of Least Privilege: Grant only the permissions required to perform a task
- Use Roles Instead of Users for Applications: Roles provide temporary credentials that rotate automatically
- Enable MFA for Privileged Users: Add an extra layer of security
- Regularly Rotate Credentials: Change passwords and access keys periodically
- Use Policy Conditions: Add constraints like IP address ranges or time windows
- Monitor with CloudTrail: Keep audit logs of all IAM actions
- 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"
}
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
}
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...
}
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
}
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
}
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"
}
}
]
})
}
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/*"
}
]
}'
IAM Policy Evaluation Logic
Understanding how AWS evaluates policies is crucial:
- By default, all requests are denied (implicit deny)
- Explicit allow in an identity-based or resource-based policy overrides the default
- Permission boundary restricts the maximum permissions
- Explicit deny overrides any allows
Explicit Deny → Deny
↓
Permission Boundary → Deny if not allowed
↓
Identity-based Policy → Allow
↓
Resource-based Policy → Allow
↓
Default → Deny
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
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/*"
}
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
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
}
}
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]
}
Conclusion
IAM is fundamental to AWS security, and mastering it takes practice. The key principles to remember:
- Always use least privilege - grant only what's needed
- Prefer roles over users - for applications and services
- Test before deploying - use simulators and analyzers
- Monitor continuously - enable CloudTrail and Config
- 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
- AWS IAM Documentation
- IAM Best Practices Guide
- AWS IAM Hands-On Repository
- AWS Security Blog
- IAM Policy Simulator
If you found this guide helpful, give it a ❤️ and follow me for more AWS and DevOps content!
Top comments (0)