AWS Basics #2: IAM — Users, Groups, Roles, Policies

11 min read

At the end of #1 AWS basics we said “don’t work as the root user.” So who do we work as? The answer is the user / role you create with IAM (Identity and Access Management).

IAM is a global service — open the console in any region and you see the same users / roles / policies. It’s also free. You can create unlimited users at no extra charge. So it’s the very first service you meet on a new AWS account.

In this post we’ll thread IAM’s four elements onto a single line and lay out the core patterns of production permission design.

Big picture — the four elements of IAM #

ElementWhat it isWho / what uses it
UserA permanent credential bound to a single human or machineConsole login, access keys
GroupA bundle of usersAttach a policy to the group; every user in it inherits
RoleA “borrow temporarily” credentialEC2, Lambda, users in another account, etc.
PolicyA JSON document of “what to allow / deny”Attached to the three above to define permissions

The key idea: the policy is the permission. Users, groups, and roles are containers for permissions, and the policies attached to them actually decide what they can do.

Policy — writing permissions in JSON #

Start with the policy. A policy is a JSON document. Getting comfortable with this shape is 80% of learning IAM.

The simplest policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Read it: allow the s3:GetObject action on every object (*) in my-bucket.

What each key means:

KeyWhat it is
VersionPolicy syntax version. Always 2012-10-17
StatementAn array of permission rules
EffectAllow or Deny
ActionWhich API call to allow / deny — <service>:<action>
ResourceWhich resource — written as an ARN
Condition (optional)Extra conditions — IP, time, tags, etc.

The shape of Action #

Like s3:GetObject, the form is <service>:<action>. Wildcards work too.

Action wildcard examples
"Action": "s3:Get*"          // every Get-* action on s3
"Action": "s3:*"             // every s3 action
"Action": ["s3:GetObject", "s3:PutObject"]  // multiple as an array

The shape of Resource #

Written as an ARN. We saw the ARN shape in #1.

Resource examples
"Resource": "arn:aws:s3:::my-bucket"           // the bucket itself
"Resource": "arn:aws:s3:::my-bucket/*"         // every object inside
"Resource": "arn:aws:s3:::my-bucket/uploads/*" // only a prefix
"Resource": "*"                                // all resources (dangerous!)

Condition — the most powerful control #

Conditions are where a policy really earns its keep.

Allow only from the office IP
{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": {
    "IpAddress": {
      "aws:SourceIp": ["203.0.113.0/24"]
    }
  }
}
Allow only on MFA-authenticated sessions
{
  "Effect": "Allow",
  "Action": "iam:*",
  "Resource": "*",
  "Condition": {
    "Bool": { "aws:MultiFactorAuthPresent": "true" }
  }
}

Conditions you’ll use often:

KeyWhat it is
aws:SourceIpRequest IP
aws:MultiFactorAuthPresentWhether MFA was used
aws:RequestTag/<key>Tag at creation
aws:ResourceTag/<key>Tag on the resource
aws:CurrentTimeTime

Allow / Deny evaluation order #

When multiple policies overlap, IAM decides in this order.

Evaluation order
1) If any explicit Deny matches → denied (end of story)
2) If any explicit Allow matches → allowed
3) Neither → denied (default)

Deny always wins. That’s why guardrails like “this user must never touch prod resources” go in as Deny. The pattern is to attach a Deny next to an Allow.

User — a permanent credential for humans / machines #

An IAM user is a separate ID, not an email. The console login URL is also separate.

IAM user console URL
https://<account-id>.signin.aws.amazon.com/console
or
https://<account-alias>.signin.aws.amazon.com/console

Creating a user #

In the console, IAM → Users → Add users.

FieldWhat it is
UsernameNot an email. An identifier like curtis, dev-bot
Console accessPassword — for humans
Programmatic accessAccess keys — for CLI / SDK (#4)
PermissionsAdd to a group, attach policies directly, or copy from another user

Best practice: don’t attach policies directly to a user. Attach them through a group. The reason in the next section.

Access keys — the riskiest credential a user holds #

An access key is a two-line string.

The shape of an access key
Access Key ID:     AKIAIOSFODNN7EXAMPLE
Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Anyone who has both can wield every permission of that user. So:

  • At most two access keys per user (briefly two during rotation)
  • Never put them in git or code (see #1 pitfall #2)
  • Rotate every 90 days (covered in detail in #6)
  • Where possible use a role instead of user access keys

Group — the unit of user management #

A group is a bundle of users. Attach a policy to the group and every user in it inherits it automatically.

Common group design
Admins         → AdministratorAccess
Developers     → PowerUserAccess + (deny IAM)
ReadOnly       → ReadOnlyAccess
Billing        → billing console only

Why through a group instead of directly? #

Imagine a new developer joining.

Direct-on-user model:

  • Pick an existing developer and copy their list of policies one by one
  • “Wait, what was that policy I added to that person last week?” → missed
  • Over time each developer’s permissions drift subtly

Group model:

  • Just add to the Developers group
  • Permission changes happen on the group’s policies and apply consistently
  • Audits are clean by group

Rule: no inline policies / direct attachment on IAM users. All permissions through groups or roles.

Role — the “borrow temporarily” credential #

A role is not someone’s credential — it’s a credential anyone (or anything) can borrow temporarily. There’s no permanent password / access key. Instead, when someone or something calls AssumeRole, AWS issues a temporary credential (valid 1–12 hours).

This is the most important part of IAM. It’s hard at first, but once you get it the real power of IAM shows up.

Where roles fit #

ScenarioWho calls AssumeRole
EC2 reaches S3The EC2 instance (automatically)
Lambda writes to DynamoDBLambda (automatically)
ECS Task calls another serviceThe task (automatically)
A user in another AWS account accesses some resource hereThat user explicitly
GitHub Actions pushes to ECRGitHub Actions Workflow via OIDC
Temporary privilege escalation (break-glass)The user explicitly

For EC2 / Lambda / ECS, AWS automatically calls AssumeRole and feeds the credential into the instance. The code just calls the SDK — it never has to deal with access keys. That’s the heart of it.

A role’s two policies #

A role has two kinds of policy attached.

1) Trust Policywho can borrow this role.

A role EC2 can borrow
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

The Principal slot says who — a service, another account, a user, an OIDC provider, etc.

2) Permission Policywhat they can do once they’ve borrowed it.

Allow S3 reads inside the role
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-bucket/*"
  }]
}

EC2 instance profile flow #

Credential flow EC2 → S3
[EC2 instance]
   │ 1) Request credentials from instance metadata (169.254.169.254)
[IAM]
   │ 2) Issue temporary credential for the role attached to the instance
[SDK inside EC2]
   │ 3) Call S3 with the credential
[S3]

The code inside EC2 doesn’t know any access key. boto3 / aws-sdk pulls the credential from metadata on its own. Key-leak incidents are blocked at the source.

Managed vs inline policies #

Policies also split by how they’re created.

KindWhat it isRecommendation
AWS managedBuilt by AWS (e.g., AdministratorAccess, ReadOnlyAccess)For getting started / standard cases
Customer managedCreated by you. Reusable across multiple users / rolesProduction standard — recommended
InlineEmbedded directly into one user / group / roleNot recommended — not reusable, hard to track

AWS managed policies you’ll use often #

NameWhat it is
AdministratorAccessEvery permission. The most dangerous after root
PowerUserAccessAlmost every permission except IAM
ReadOnlyAccessRead across every service
BillingThe billing console
<Service>FullAccess (e.g., AmazonS3FullAccess)All of one service
<Service>ReadOnlyAccessRead of one service

The usual flow is: start managed → tighten over time to customer-managed.

Production permission design — patterns that hold up #

The skeleton of a permission design that works even on a small team.

1) Four groups + three roles #

UnitNameWhat it is
GroupAdminsAdministratorAccess (MFA enforced)
GroupDevelopersPowerUserAccess + deny IAM
GroupReadOnlyReadOnlyAccess
GroupBillingBilling / cost
RoleEC2-AppRoleWhat the app uses (S3, RDS, etc.)
RoleLambda-WorkerRoleFor Lambda
RoleCICDRoleBorrowed by GitHub Actions / CodeBuild

2) Permission boundaries #

A guard that says “whatever this IAM user does, it’s bounded by this.” Blocks the incident where a junior developer creates a new policy to grant themselves more access.

Pattern
Developers group → PowerUserAccess
   ↓ and a permission boundary on every user
Permission boundary → "DevOnly" — can only create dev/* resources

3) Enforce MFA #

Root + every IAM user. The shape of the enforcement policy is in #6.

4) Separate user from role #

Users = humans only. Machines all use roles. Once that’s clear, access-key incidents almost stop.

5) Guardrails via conditions #

Block developers from touching prod-* tagged resources
{
  "Effect": "Deny",
  "Action": "*",
  "Resource": "*",
  "Condition": {
    "StringEquals": { "aws:ResourceTag/env": "prod" }
  }
}

If you tag consistently, environment separation comes from a single line of policy.

Policy Simulator — the verification tool #

The IAM Policy Simulator (policysim.aws.amazon.com) simulates “can this action go through?” for a given user / group / role. Use it to confirm that your policy changes do what you intended.

Flow
1) Pick a user or role
2) Pick the action to simulate (e.g., s3:DeleteObject)
3) Optionally enter a resource ARN
4) "Run Simulation" → Allowed / Denied with the policies that influenced the result

The IAM Access Analyzer in #6 helps with similar audits.

Creating an IAM user — hands-on #

By this point, go ahead and create one IAM user for yourself. From #4 CLI onward you’ll need this user / role.

Steps
1. Console → IAM → Users → Add users
2. Username: your name (e.g., curtis)
3. Console access: enabled, auto-generated password
4. Attach policies directly: AdministratorAccess (for learning — narrow it later)
5. Right after creation → enable MFA immediately (enforcement in #6)
6. Access keys: issue one for the CLI
7. Download the .csv — the secret only appears on that screen

Right after this, move on to #3 Cost management and #6 Security basics.

Common pitfalls #

1) Policies attached directly to users #

Up to ten people it looks fine, but at thirty, permissions start drifting in subtle ways. After a breach you can’t say who can do what. Round them up into groups.

2) Resource: "*" everywhere #

Convenient at first, but blast radius on a breach is unbounded. Tighten with ARNs wherever you can.

3) Inline policies #

Convenient to add on the spot, but a month later no one knows who added what. Not reusable either. Use customer-managed policies — they get a name and versioning.

4) Access keys baked into EC2 #

Code that solves EC2 → S3 access using access keys. Move it to an instance profile (role). Forget rotation once and the incident is permanently latent.

5) “Test users” left lying around #

Former employees’ keys, bot users someone created and forgot — they all accumulate. Set up automatic deactivation or deletion for users idle for 30 or 90 days. The IAM Credential Report shows the whole picture at once.

6) Access keys on the root user #

#1 pitfall #2 — the most common incident. Root is console + MFA only, no access keys, ever.

Wrap-up #

What we covered:

  • The four elements of IAM — User (permanent credential for human / machine), Group (bundle of users), Role (borrow temporarily), Policy (JSON permission document)
  • Shape of a policyEffect + Action + Resource (+ Condition). Deny beats Allow
  • No direct policy attachment on users → bundle into groups
  • A role’s two policies — Trust Policy (who can borrow) + Permission Policy (what they can do once they have it)
  • EC2 / Lambda / ECS get credentials via instance profile / execution role — keep keys out of code
  • Managed vs inline — customer-managed is the production standard
  • Permission design patterns — four groups + three roles, permission boundaries, MFA enforcement, user / role separation, tag + condition guardrails
  • Pitfalls — direct user policies, Resource:*, inline policies, access keys inside EC2, idle users, root keys

Next — cost management #

Now that you have an IAM user, next up is billing. The biggest incident in the first few days is idle resources growing the bill.

#3 Cost management — Billing alerts, Cost Explorer, Free Tier covers the billing alerts you must turn on right after signup, monitoring the Free Tier ceiling, and the Cost Explorer / tag strategy that scales into production.

X