Prerequisites
- An AWS account (the free tier is enough for this lab)
- AWS Console access, and optionally the AWS CLI configured with an admin credential
What You'll Learn
- Explain the difference between an IAM user, group, and role
- Write a least-privilege JSON policy scoped to a specific resource and action
- Trace how AWS evaluates an explicit deny vs. an implicit deny vs. an allow
- Assume a role using the AWS CLI and know when to use a role instead of a user
Theory
IAM has four core building blocks. A user is a permanent identity with long-term credentials (console password and/or access keys) — meant for a specific person or a legacy application. A group is just a named collection of users that policies can be attached to once, instead of per-user. A rolehas no long-term credentials at all — it's assumed temporarily (via AWS STS) by a user, an AWS service like EC2/Lambda, or an external identity provider, and issues short-lived, auto-expiring credentials. A policyis the actual JSON document defining what's allowed or denied.
Why roles are preferred for workloads
An access key embedded in application code or an EC2 instance is a long-lived secret that, if leaked, works until manually revoked. A role attached to an EC2 instance profile issues credentials that auto-rotate every few hours via the instance metadata service — even if the workload is compromised, the exposure window is bounded, and there's no static secret to leak in the first place.
How a policy statement is structured
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOnlyOnOneBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
]
}
]
}This grants read-only access to exactly one bucket — not s3:* on *, which is the single most common IAM mistake. Scoping both the Action list and the Resource ARN is what makes a policy "least privilege" rather than a rubber stamp.
Architecture
AWS collects every applicable policy — identity-based, resource-based, permission boundaries, and (in an organization) SCPs — and evaluates them together using one fixed precedence rule:
An explicit Deny anywhere always wins; otherwise an explicit Allow is required; the default is deny
Hands-On Lab
Create a least-privilege role and assume it via the CLI:
# 1. Create a trust policy allowing your own account to assume this role
cat > trust-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role \
--role-name s3-readonly-role \
--assume-role-policy-document file://trust-policy.json
# 2. Attach a scoped permission policy (use the JSON from Theory above, saved as read-policy.json)
aws iam put-role-policy \
--role-name s3-readonly-role \
--policy-name S3ReadOnlyOneBucket \
--policy-document file://read-policy.json
# 3. Assume the role and get temporary credentials
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/s3-readonly-role \
--role-session-name test-session
# 4. Use the returned AccessKeyId/SecretAccessKey/SessionToken to confirm
# you can read from the bucket but NOT write to it or read any other bucketBest Practices
Start from zero, add only what's used
Attach the narrowest policy that works, then use IAM Access Advisor (per-role, shows last-accessed services) after a few weeks of real usage to trim anything that was granted but never actually called.Use roles for every workload, users only for humans
No EC2 instance, Lambda function, or ECS task should ever have a static access key baked in. If it needs AWS API access, attach a role — this is the single highest-leverage IAM habit.Common Mistakes
Attaching AdministratorAccess to unblock a stuck deployment
It's the fastest way to make a permissions error go away — and the fastest way to leave a resource with far more access than it needs long after the original blocker is forgotten. Add the specific missing action instead.Forgetting that S3 bucket policies and IAM policies are evaluated together
A bucket policy denying public access and an IAM policy allowing `s3:GetObject` don't conflict resolve in the IAM policy's favor — an explicit Deny anywhere (including a resource-based bucket policy) overrides any Allow.Troubleshooting
"AccessDenied" despite an Allow policy attached: use the IAM Policy Simulator or CloudTrail's "Event history" to see the exact API call and the effective decision — check for an explicit Deny in a permission boundary or SCP first, since those silently override an Allow at the identity level without any error hinting at their existence.
A role can't be assumed: confirm the trust policy's Principal matches the exact caller (account root vs. a specific user/role ARN), and that the calling identity has sts:AssumeRolepermission on that specific role's ARN — both sides (trust policy AND caller permission) must allow it.
Interview Questions
Frequently Asked Questions
Summary
Users are for people, roles are for workloads and temporary access, groups organize users, and policies are JSON documents scoped by Action and Resource. AWS evaluates every applicable policy together with one rule: an explicit deny anywhere always wins, otherwise an explicit allow is required, and the default is deny.