Skip to content

Lab 2 · IAM Users, Roles & Policies

Security · 30% · Est. time: 30–40 min · Cost: \$0 (IAM is free; the optional EC2 step is Free-Tier eligible)

Goal. Feel the evaluation engine in your hands: create a group + user, watch an explicit deny beat an allow, build a role an EC2 instance can assume, and cap a role with a permissions boundary. Everything here maps directly to Topic 1.

Before you start

  • Do this as an admin IAM user or role, not the root user.
  • IAM itself costs nothing. The only thing in this lab that could bill is the optional EC2 instance in Step 5 — it's Free-Tier eligible (t2.micro/t3.micro, 750 hrs/mo for 12 months), but terminate it at the end regardless.
  • If you haven't set a billing alarm yet, do Lab 12 first.

What you'll build

  1. A Developers group with read-only S3 access.
  2. A user in that group, plus an explicit deny that overrides the group's allow.
  3. An EC2 role (instance profile) granting S3 read access — no keys anywhere.
  4. A permissions boundary that caps a role regardless of its attached policy.

Step 1 — Create a customer-managed policy

  1. Open the IAM console → left nav PoliciesCreate policy.
  2. Click the JSON tab and paste:
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "ReadOnlyS3",
          "Effect": "Allow",
          "Action": ["s3:GetObject", "s3:ListBucket"],
          "Resource": "*"
        }
      ]
    }
    
  3. NextPolicy name: Lab-S3ReadOnlyCreate policy.
cat > s3-read.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "ReadOnlyS3", "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"], "Resource": "*" }
  ]
}
EOF

aws iam create-policy \
  --policy-name Lab-S3ReadOnly \
  --policy-document file://s3-read.json

Tip

In the real world you'd scope Resource to specific bucket ARNs, not *. We use * to keep the lab short — note this is not least privilege.


Step 2 — Create a group and attach the policy

  1. IAM → User groupsCreate group.
  2. Group name: Developers.
  3. Under Attach permissions policies, search Lab-S3ReadOnly, tick it.
  4. Create group.
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

aws iam create-group --group-name Developers

aws iam attach-group-policy \
  --group-name Developers \
  --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Lab-S3ReadOnly

Step 3 — Create a user and see the group's permissions apply

  1. IAM → UsersCreate user.
  2. User name: lab-dev. (Leave console access off — we only need programmatic testing, and real humans should use IAM Identity Center, not IAM users.)
  3. NextAdd user to group → tick DevelopersNextCreate user.
aws iam create-user --user-name lab-dev
aws iam add-user-to-group --user-name lab-dev --group-name Developers

The user now inherits s3:GetObject / s3:ListBucket from the group — it has no policy of its own yet. Confirm with the simulator in the next step.


Step 4 — Watch an explicit deny override the allow

This is the single most important IAM concept, and you can prove it without any real resources using the Policy Simulator.

  1. Attach an inline deny to the user:

    1. IAM → Userslab-devPermissions tab → Add permissionsCreate inline policy.
    2. JSON tab, paste:
      {
        "Version": "2012-10-17",
        "Statement": [
          { "Effect": "Deny", "Action": "s3:GetObject", "Resource": "*" }
        ]
      }
      
    3. Next → name DenyS3GetCreate policy.
    cat > deny-get.json <<'EOF'
    { "Version": "2012-10-17",
      "Statement": [ { "Effect": "Deny", "Action": "s3:GetObject", "Resource": "*" } ] }
    EOF
    
    aws iam put-user-policy \
      --user-name lab-dev \
      --policy-name DenyS3Get \
      --policy-document file://deny-get.json
    
  2. Open the IAM Policy Simulator: https://policysimulator.aws.amazon.com/ (or IAM console → user → Simulate shortcuts).

  3. Pick user lab-dev, service S3, actions GetObject and ListBucket, then Run simulation.

Observe:

  • s3:ListBucketallowed (group grants it, nothing denies it).
  • s3:GetObjectdenied — the inline Deny overrides the group's Allow.

✅ You just demonstrated Explicit Deny > Allow. No object was ever created — the simulator evaluates policies exactly as the live engine does.


Step 5 — Create a role for EC2 (instance profile)

Goal: give an EC2 instance S3 read access without any stored keys.

  1. IAM → RolesCreate role.
  2. Trusted entity type: AWS service. Use case: EC2. Next.
  3. Attach Lab-S3ReadOnly. Next.
  4. Role name: Lab-EC2-S3ReadCreate role. (The console auto-creates a matching instance profile.)
cat > ec2-trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole" }
  ]
}
EOF

aws iam create-role --role-name Lab-EC2-S3Read \
  --assume-role-policy-document file://ec2-trust.json

aws iam attach-role-policy --role-name Lab-EC2-S3Read \
  --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Lab-S3ReadOnly

# CLI-only: the instance profile the console makes for you
aws iam create-instance-profile --instance-profile-name Lab-EC2-S3Read
aws iam add-role-to-instance-profile \
  --instance-profile-name Lab-EC2-S3Read --role-name Lab-EC2-S3Read

Notice the trust policy names ec2.amazonaws.com as the principal allowed to assume the role — that's the "who can assume" half; Lab-S3ReadOnly is the "what it can do" half.

Optional — prove it end-to-end (Free-Tier EC2)

  1. Launch a t3.micro (or t2.micro) Amazon Linux instance; under Advanced details → IAM instance profile, pick Lab-EC2-S3Read.
  2. Connect via EC2 Instance Connect / Session Manager (no SSH key needed).
  3. Run — note there are no credentials on the box:
    aws sts get-caller-identity        # shows the assumed-role ARN
    aws s3 ls                          # works — read granted by the role
    curl http://169.254.169.254/latest/meta-data/iam/security-credentials/  # IMDS (use IMDSv2)
    
  4. Terminate the instance immediately after (EC2 → Instance state → Terminate).

Step 6 — Cap a role with a permissions boundary

Show that a boundary limits a role below its attached policy.

  1. Create a boundary policy (IAM → Policies → Create policy → JSON):
    {
      "Version": "2012-10-17",
      "Statement": [
        { "Effect": "Allow", "Action": "s3:*", "Resource": "*" }
      ]
    }
    
    Name it Lab-Boundary-S3Only.
  2. Create a role Lab-Boundary-Demo (trusted entity: your account / same as above) and attach the AWS-managed AdministratorAccess policy to it.
  3. On that role → Permissions boundarySet boundary → choose Lab-Boundary-S3Only.
aws iam create-policy --policy-name Lab-Boundary-S3Only \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}'

aws iam create-role --role-name Lab-Boundary-Demo \
  --assume-role-policy-document file://ec2-trust.json \
  --permissions-boundary arn:aws:iam::$ACCOUNT_ID:policy/Lab-Boundary-S3Only

aws iam attach-role-policy --role-name Lab-Boundary-Demo \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

Simulate ec2:StopInstances and s3:PutObject for Lab-Boundary-Demo:

  • s3:PutObjectallowed (both the admin policy and the boundary allow S3).
  • ec2:StopInstancesdeniedAdministratorAccess allows it, but the boundary doesn't, and effective permissions are the intersection.

✅ You just proved a boundary caps even AdministratorAccess.


Teardown

IAM resources don't bill, but clean up so your account stays tidy and the credential report stays meaningful.

Delete in this order (a policy can't be deleted while attached):

  1. Terminate the optional EC2 instance (if launched).
  2. Roles: Lab-EC2-S3Read, Lab-Boundary-Demo (detach policies first if prompted).
  3. Instance profile Lab-EC2-S3Read (deleted with the role in the console).
  4. User lab-dev (remove from group + delete inline policy happens on user delete).
  5. Group Developers.
  6. Policies: Lab-S3ReadOnly, Lab-Boundary-S3Only.
# EC2 (if launched): terminate via console or `aws ec2 terminate-instances --instance-ids ...`

aws iam remove-role-from-instance-profile --instance-profile-name Lab-EC2-S3Read --role-name Lab-EC2-S3Read
aws iam delete-instance-profile --instance-profile-name Lab-EC2-S3Read
aws iam detach-role-policy --role-name Lab-EC2-S3Read --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Lab-S3ReadOnly
aws iam delete-role --role-name Lab-EC2-S3Read

aws iam detach-role-policy --role-name Lab-Boundary-Demo --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam delete-role --role-name Lab-Boundary-Demo

aws iam delete-user-policy --user-name lab-dev --policy-name DenyS3Get
aws iam remove-user-from-group --user-name lab-dev --group-name Developers
aws iam delete-user --user-name lab-dev
aws iam delete-group --group-name Developers

aws iam delete-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Lab-S3ReadOnly
aws iam delete-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Lab-Boundary-S3Only

✅ What you should understand now

  • A group carries permissions for its members but is not an identity you can log in as.
  • An explicit Deny overrides any Allow, no matter where the allow comes from — you saw it in the simulator (Explicit Deny > Allow > default Deny).
  • The Policy Simulator lets you test evaluation logic with zero real resources or risk.
  • An EC2 role via an instance profile delivers temporary credentials with nothing stored on the instance — the trust policy says who can assume, the permission policy says what it can do.
  • A permissions boundary caps effective permissions to the intersection with the attached policy — it can shrink even AdministratorAccess, and it never grants anything.

Related: Learn — IAM Deep Dive · Revise — IAM · Mock Exam 1