Skip to content

AWS IAM Fundamentals: Users, Roles, Policies & Least Privilege

IAM is the single most misconfigured service in AWS — understand how policies are actually evaluated before you write another one.

IAMAWS CLISTS
3 min readBeginner
Lab time: 20-30 min
RR

Ranjith R

Linux SysAdmin & Cloud Engineer

Published July 1, 2026Updated July 6, 2026
Share
Section 01

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
Section 02

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
Section 03

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.

Section 04

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

Section 05

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 bucket
Section 07

Best 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.
Section 08

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.
Section 09

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.

Section 10

Interview Questions

An IAM user represents a permanent identity (a person or application) with its own long-term credentials (password and/or access keys). An IAM role has no long-term credentials — it's assumed temporarily by a user, an AWS service (like an EC2 instance or Lambda function), or an external identity, and grants short-lived, auto-rotating credentials via STS. Roles are the recommended way to grant an EC2 instance or Lambda function permissions, instead of embedding a user's access keys in code.
See all 20 AWS interview questions
FAQ

Frequently Asked Questions

An IAM (identity-based) policy is attached to a user, group, or role and defines what that identity can do. A resource-based policy (like an S3 bucket policy) is attached to the resource itself and can grant access to other accounts or principals directly, without those principals needing an IAM policy in their own account. Both are evaluated together.
Section 12

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.