AWS IAM Mastery: Users, Roles, Policies, and Best Practices
AWS IAM Mastery: Users, Roles, Policies, and Best Practices
Introduction
You’ve just created your first AWS account. You have root access to everything—EC2 instances, S3 buckets, databases, billing. It feels powerful. Then reality hits: your development team needs access to deploy applications, your DevOps engineer needs to manage infrastructure, your data analyst needs read-only access to S3, and your auditor needs to review logs without changing anything.
How do you grant the right level of access to each person without sharing your root password? How do you let your application running on EC2 access S3 buckets without hardcoding credentials? How do you ensure a compromised key can’t delete your entire production database?
AWS Identity and Access Management (IAM) is the answer. It’s not just about creating users and passwords—it’s the foundational security layer that controls who (or what) can do what in your AWS account. Understanding IAM isn’t optional; it’s the difference between a secure cloud infrastructure and a data breach waiting to happen.
In this guide, we’ll master IAM from first principles: users, groups, roles, policies, permissions boundaries, and the security best practices that separate amateur AWS usage from production-grade infrastructure.
What Is AWS IAM?
IAM is AWS’s identity and access management service that enables you to:
✅ Control who can authenticate (sign in) to your AWS account
✅ Control what authenticated identities can do (authorization)
✅ Manage access for humans (users) and applications (roles)
✅ Enforce security policies at scale across your organization
✅ Audit access through detailed logs (CloudTrail integration)
IAM Is Global
Unlike EC2 (region-specific) or S3 (bucket in specific regions), IAM is a global service. When you create an IAM user, role, or policy, it exists across all AWS regions simultaneously.
IAM Is Free
AWS does not charge for IAM usage. You only pay for the AWS resources that IAM identities access (EC2, S3, RDS, etc.).
Core IAM Concepts
1. IAM Users
An IAM user represents a person or service that interacts with AWS.
When to create IAM users:
- Individual developers who need AWS Console access
- CI/CD pipelines that deploy via CLI/SDK
- Third-party monitoring tools accessing your account
Anatomy of an IAM user:
1
2
3
4
5
6
7
8
User: john.doe@company.com
├── Username: john.doe
├── Password: (for AWS Console access)
├── Access Keys: (for CLI/SDK access)
│ ├── Access Key ID: AKIAIOSFODNN7EXAMPLE
│ └── Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
├── Permissions: (via attached policies)
└── MFA Device: (optional but recommended)
Creating an IAM user via AWS CLI:
1
2
3
4
5
6
7
8
9
10
11
# Create user
aws iam create-user --user-name john.doe
# Create login profile (console password)
aws iam create-login-profile \
--user-name john.doe \
--password 'TempPassword123!' \
--password-reset-required
# Create access key (for CLI/SDK)
aws iam create-access-key --user-name john.doe
Output:
1
2
3
4
5
6
7
8
{
"AccessKey": {
"UserName": "john.doe",
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"Status": "Active",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
}
⚠️ Security note: Store the SecretAccessKey securely—it’s shown only once!
2. IAM Groups
Groups are collections of IAM users. Instead of attaching policies to each user individually, attach policies to groups.
Example group structure:
1
2
3
4
5
6
7
8
9
10
11
Groups:
├── Developers
│ ├── john.doe
│ ├── jane.smith
│ └── Policies: EC2FullAccess, S3ReadOnly
├── DevOps
│ ├── alice.admin
│ └── Policies: AdministratorAccess
└── DataAnalysts
├── bob.data
└── Policies: S3ReadOnly, AthenaFullAccess
Creating groups and adding users:
1
2
3
4
5
6
7
8
9
10
11
12
# Create group
aws iam create-group --group-name Developers
# Add user to group
aws iam add-user-to-group \
--user-name john.doe \
--group-name Developers
# Attach policy to group
aws iam attach-group-policy \
--group-name Developers \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess
Best practice: Use groups to manage permissions, not individual user policies.
3. IAM Roles
Roles are identities with permissions, but unlike users, they’re assumed by:
- AWS services (EC2, Lambda, ECS)
- Users from another AWS account
- Federated users (via SAML, OIDC)
- Applications running on EC2
Key difference: Users vs Roles
| Aspect | IAM User | IAM Role |
|---|---|---|
| Purpose | Long-term identity for a person/service | Temporary identity assumed on-demand |
| Credentials | Permanent (password/access keys) | Temporary security credentials (rotated automatically) |
| Use case | Human accessing AWS Console/CLI | EC2 instance accessing S3, Lambda accessing DynamoDB |
Why roles are more secure:
- No hardcoded credentials in code
- Automatic credential rotation (typically every 15 minutes - 12 hours)
- Least privilege via temporary permissions
Example: EC2 instance accessing S3
❌ Bad approach (hardcoded credentials):
1
2
3
4
5
6
7
# In your Rails app on EC2
Aws.config.update(
credentials: Aws::Credentials.new(
'AKIAIOSFODNN7EXAMPLE',
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
)
)
Problems:
- Credentials in code (can be leaked via Git)
- Manual rotation required
- Broad permissions risk
✅ Good approach (IAM role):
- Create IAM role with S3 access:
```bash
Create trust policy (who can assume this role)
cat > trust-policy.json «EOF { “Version”: “2012-10-17”, “Statement”: [{ “Effect”: “Allow”, “Principal”: { “Service”: “ec2.amazonaws.com” }, “Action”: “sts:AssumeRole” }] } EOF
Create role
aws iam create-role
–role-name EC2-S3-Access-Role
–assume-role-policy-document file://trust-policy.json
Attach S3 policy
aws iam attach-role-policy
–role-name EC2-S3-Access-Role
–policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Create instance profile (wrapper for EC2)
aws iam create-instance-profile
–instance-profile-name EC2-S3-Access-Profile
Add role to instance profile
aws iam add-role-to-instance-profile
–instance-profile-name EC2-S3-Access-Profile
–role-name EC2-S3-Access-Role
1
2
3
4
5
6
7
2. Attach role to EC2 instance:
```bash
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--iam-instance-profile Name=EC2-S3-Access-Profile
- Rails app code (no credentials needed!):
1 2 3
# AWS SDK automatically uses instance role credentials s3 = Aws::S3::Client.new(region: 'us-east-1') s3.list_buckets
The AWS SDK automatically:
- Detects it’s running on EC2
- Fetches temporary credentials from instance metadata
- Rotates credentials before expiry
4. IAM Policies
Policies are JSON documents that define permissions. They specify:
- What actions are allowed/denied (
s3:GetObject,ec2:StartInstances) - Which resources the actions apply to (
arn:aws:s3:::my-bucket/*) - Under what conditions (IP restrictions, MFA required, time-based)
Policy structure:
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
Policy types:
| Type | Description | Example |
|---|---|---|
| AWS Managed | Pre-built by AWS, cover common use cases | AmazonS3ReadOnlyAccess |
| Customer Managed | Custom policies you create | MyApp-S3-Access |
| Inline | Embedded directly in user/role/group | (Avoid—hard to manage) |
Policy evaluation logic:
1
2
3
4
5
6
7
Request → IAM evaluates all applicable policies
↓
Explicit Deny? → ❌ DENY (overrides everything)
↓ No
Explicit Allow? → ✅ ALLOW
↓ No
❌ DENY (default deny)
Key principle: Explicit deny always wins.
5. Policy Example: Least Privilege S3 Access
Let’s create a policy that allows:
- Read/write access to
my-app-uploads/prefix - Read-only access to
my-app-public/prefix - No access to anything else
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-bucket",
"Condition": {
"StringLike": {
"s3:prefix": [
"uploads/*",
"public/*"
]
}
}
},
{
"Sid": "ReadWriteUploads",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-app-bucket/uploads/*"
},
{
"Sid": "ReadOnlyPublic",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-app-bucket/public/*"
}
]
}
Create and attach the policy:
1
2
3
4
5
6
7
8
9
# Save policy as custom-s3-policy.json
aws iam create-policy \
--policy-name MyApp-S3-Limited-Access \
--policy-document file://custom-s3-policy.json
# Attach to user
aws iam attach-user-policy \
--user-name john.doe \
--policy-arn arn:aws:iam::123456789012:policy/MyApp-S3-Limited-Access
Policy Variables and Conditions
IAM policies support dynamic variables and conditions for fine-grained access control.
Policy Variables
Use ${aws:username} to create user-specific permissions:
1
2
3
4
5
6
7
8
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/${aws:username}/*"
}]
}
Result: Each user can only access their own folder in the bucket (my-bucket/john.doe/, my-bucket/jane.smith/).
Condition Keys
Enforce security requirements:
Require MFA for critical operations:
1
2
3
4
5
6
7
8
9
10
11
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ec2:TerminateInstances",
"Resource": "*",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}]
}
Restrict access by IP address:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}]
}
Time-based access (business hours only):
1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "rds:*",
"Resource": "*",
"Condition": {
"DateGreaterThan": {"aws:CurrentTime": "2024-01-01T09:00:00Z"},
"DateLessThan": {"aws:CurrentTime": "2024-12-31T17:00:00Z"}
}
}]
}
IAM Best Practices
1. Never Use Root Account
Your AWS root account has unrestricted access to everything, including billing.
What to do:
- Enable MFA on root account
- Create IAM admin user with
AdministratorAccesspolicy - Lock root credentials in a secure vault
- Use root account only for:
- Changing account settings
- Closing AWS account
- Restoring IAM admin access if lost
2. Enable MFA Everywhere
Multi-factor authentication adds a second layer of security.
Enable MFA via CLI:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Create virtual MFA device
aws iam create-virtual-mfa-device \
--virtual-mfa-device-name john-doe-mfa \
--outfile qr-code.png \
--bootstrap-method QRCodePNG
# Scan QR code with Google Authenticator
# Get two consecutive codes, then enable MFA
aws iam enable-mfa-device \
--user-name john.doe \
--serial-number arn:aws:iam::123456789012:mfa/john-doe-mfa \
--authentication-code-1 123456 \
--authentication-code-2 789012
3. Use Roles for Applications
Never hardcode AWS credentials in application code or config files.
For EC2/ECS/Lambda: Use IAM roles (as shown earlier)
For local development: Use AWS CLI profiles with named credentials
Local development setup:
1
2
3
4
5
6
7
8
9
# Configure named profile
aws configure --profile myapp-dev
AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key: wJalrXUtnFEMI/...
Default region name: us-east-1
# Use in application
export AWS_PROFILE=myapp-dev
rails server
4. Apply Least Privilege
Grant only the permissions required for the task, nothing more.
❌ Bad: AdministratorAccess for everyone
✅ Good: Custom policies with minimal required actions
Example: Deploy-only role for CI/CD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::my-app-deployments/*"
},
{
"Effect": "Allow",
"Action": [
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "arn:aws:ecs:us-east-1:123456789012:service/my-cluster/my-service"
}
]
}
This role can only deploy to specific S3 bucket and ECS service—nothing else.
5. Rotate Credentials Regularly
Access keys should be rotated every 90 days.
Check access key age:
1
aws iam list-access-keys --user-name john.doe
Rotate access keys:
1
2
3
4
5
6
7
8
9
10
# Create new key
aws iam create-access-key --user-name john.doe
# Update applications to use new key
# Test thoroughly
# Delete old key (after confirming new key works)
aws iam delete-access-key \
--user-name john.doe \
--access-key-id AKIAIOSFODNN7EXAMPLE
6. Use IAM Access Analyzer
AWS IAM Access Analyzer identifies resources shared with external entities.
Enable Access Analyzer:
1
2
3
aws accessanalyzer create-analyzer \
--analyzer-name my-account-analyzer \
--type ACCOUNT
Review findings:
1
2
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/my-account-analyzer
7. Monitor with CloudTrail
Every IAM action is logged in CloudTrail.
Query recent IAM changes:
1
2
3
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateUser \
--max-results 10
Common IAM Patterns
Pattern 1: Cross-Account Access
Allow users from Account A to access resources in Account B.
In Account B (resource account), create role:
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}]
}
In Account A, allow users to assume the role:
1
2
3
4
5
6
7
8
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::222222222222:role/CrossAccountS3Access"
}]
}
Assume role from Account A:
1
2
3
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/CrossAccountS3Access \
--role-session-name my-session
Pattern 2: Service Control Policies (SCPs)
In AWS Organizations, SCPs set permission boundaries for entire accounts.
Example: Prevent EC2 termination in production account
1
2
3
4
5
6
7
8
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "ec2:TerminateInstances",
"Resource": "*"
}]
}
Apply to organization unit:
1
2
3
aws organizations attach-policy \
--policy-id p-12345678 \
--target-id ou-prod-12345
Troubleshooting IAM Issues
Problem: “Access Denied” Error
Diagnose with IAM Policy Simulator:
1
2
3
4
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:user/john.doe \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/file.txt
Common causes:
- Missing permission in policy
- Explicit deny somewhere (check SCPs, permission boundaries)
- Resource-based policy (S3 bucket policy) blocking access
- Incorrect resource ARN in policy
Problem: Role Cannot Be Assumed
Check trust policy:
1
aws iam get-role --role-name MyRole
Verify Principal allows your service/account to assume it.
Problem: Policy Too Complex
Use managed policies when possible:
1
aws iam list-policies --scope AWS | grep S3
Break large inline policies into multiple managed policies.
Real-World Example: Rails App on EC2
Let’s design IAM architecture for a Rails application:
Requirements:
- EC2 instances need S3 access (user uploads)
- EC2 instances need RDS access (application database)
- Developers need SSH and deployment access
- CloudWatch logging enabled
IAM architecture:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. EC2 Instance Role: Rails-App-EC2-Role
Policies:
- S3 access to uploads bucket
- CloudWatch Logs write access
- Systems Manager (for SSH via Session Manager)
2. RDS Security: Database security groups (not IAM)
- SG allows traffic only from EC2 security group
3. Developer IAM Group: Rails-Developers
Policies:
- EC2 read/write (for deployments)
- S3 read/write (uploads bucket only)
- CloudWatch Logs read (for debugging)
- Systems Manager StartSession (for SSH)
4. CI/CD Role: CircleCI-Deploy-Role
Policies:
- S3 PutObject (for assets)
- EC2 DescribeInstances, StartInstances, StopInstances
- Systems Manager SendCommand (for deployment scripts)
Implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. Create EC2 role
aws iam create-role --role-name Rails-App-EC2-Role \
--assume-role-policy-document file://ec2-trust-policy.json
aws iam attach-role-policy --role-name Rails-App-EC2-Role \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
aws iam put-role-policy --role-name Rails-App-EC2-Role \
--policy-name S3-Uploads-Access \
--policy-document file://s3-uploads-policy.json
# 2. Create developer group
aws iam create-group --group-name Rails-Developers
aws iam attach-group-policy --group-name Rails-Developers \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess
Conclusion
AWS IAM is not just about creating users and assigning passwords—it’s the foundational security layer that protects your entire cloud infrastructure. By mastering IAM users, groups, roles, and policies, you gain precise control over who can do what in your AWS environment.
The shift from hardcoded credentials to IAM roles, from overly permissive AdministratorAccess to least-privilege custom policies, and from password-only auth to MFA-enforced access transforms your AWS account from a security liability into a hardened fortress.
Start with the root account: lock it down, enable MFA, and never touch it again. Build IAM users and groups for your team. Create roles for your applications. Audit with Access Analyzer and CloudTrail. Iterate toward least privilege.
IAM is complex, but every hour invested in understanding it saves weeks of incident response down the road. Master IAM, and you master AWS security.
Suggested Reading
- AWS IAM Official Documentation
- IAM Best Practices - AWS Security
- AWS Security Token Service (STS) Documentation
- IAM Policy Reference
- AWS Organizations and SCPs
- IAM Access Analyzer