IAM — Users, Groups, Roles, Policies
Sort out IAM's four elements — users · groups · roles · policies — that decide who you work as on AWS, all in one go. Covers JSON policy syntax, the essence of AssumeRole, and permission-design patterns that hold up even in a small team.
At the end of Chapter 1 Getting Started with AWS we said “don’t work as the root user.” So who should you work as? The answer is the users and roles you create with IAM (Identity and Access Management). This chapter starts the first setup you must complete right after sign-up.
IAM is a global service. No matter which Region you open the console in, you see the same users / roles / policies. And it’s free. You can create unlimited users at no extra cost. That’s why it’s the first service you should learn when you start using AWS.
In this chapter we sort out IAM’s four elements in one go and look at the core permission-design patterns that hold up in operations. The users and roles you create here show up again from Chapter 4 CLI and SDK onward, and the story gets stricter with MFA and least privilege in Chapter 6 security basics.
The big picture — IAM’s four elements #
| Element | What | Who / what uses it |
|---|---|---|
| User | A permanent credential tied to one person or one machine | Console login, access keys |
| Group | A bundle of users | Attach a policy to a group and it applies to all users inside |
| Role | A credential borrowed temporarily | EC2, Lambda, users in another account, etc. |
| Policy | A JSON document of what to allow / deny | Attached to the three above to define permissions |
The key point is that a policy is the permission. Users / groups / roles are containers that hold permissions, and the policy inside decides what you can actually do.
Policy — writing permissions in JSON #
A policy is a JSON document. Getting comfortable with this shape is 80% of learning IAM.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}Read it this way: this allows the s3:GetObject action on all objects (*) in my-bucket. The meaning of each key:
| Key | What |
|---|---|
Version | Policy syntax version. Always 2012-10-17 |
Statement | An array of permission rules |
Effect | Allow or Deny |
Action | Which API calls to allow / deny — <service>:<action> |
Resource | On which resource — by ARN |
Condition (optional) | Extra conditions — IP, time, tags, etc. |
The form of Action #
Like s3:GetObject, it’s the form <service>:<action>. Wildcards are allowed too.
"Action": "s3:Get*" // all Get actions of s3
"Action": "s3:*" // all actions of s3
"Action": ["s3:GetObject", "s3:PutObject"] // several as an arrayThe form of Resource #
Written as an ARN. We saw the shape of ARNs in Chapter 1.
"Resource": "arn:aws:s3:::my-bucket" // the bucket itself
"Resource": "arn:aws:s3:::my-bucket/*" // all objects in the bucket
"Resource": "arn:aws:s3:::my-bucket/uploads/*" // only a specific prefix
"Resource": "*" // all resources (dangerous)Condition — the most powerful control #
Conditions are what make a policy truly powerful.
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24"]
}
}
}{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}Commonly used condition keys:
| Key | What |
|---|---|
aws:SourceIp | Request IP |
aws:MultiFactorAuthPresent | Whether MFA is used |
aws:RequestTag/<key> | Tag at creation time |
aws:ResourceTag/<key> | Resource tag |
aws:CurrentTime | Time |
Allow / Deny evaluation order #
When multiple policies overlap, IAM decides in the following order.
1) If any explicit Deny exists → deny (end)
2) If any explicit Allow exists → allow
3) If neither → deny (default)Deny always wins. That’s why guardrails like “this user must never touch prod resources” are written as Deny. The pattern is to pair a Deny with an Allow policy.
User — a permanent credential for a person / machine #
An IAM user is a separate ID, not an email address. It also gets its own console login URL.
https://<account-id>.signin.aws.amazon.com/console
or
https://<account-alias>.signin.aws.amazon.com/consoleCreating a user #
In the console, go to IAM → Users → Add users.
| Item | What |
|---|---|
| User name | Not an email. An identifier like curtis, dev-bot |
| Console access | Password — for people |
| Programmatic access | Access keys — for CLI / SDK (Chapter 4 CLI and SDK) |
| Permissions | Add to a group, attach policies directly, or copy from another user |
The best practice is not to attach a policy directly to a user. Attach it through a group. The reason is covered in the next section.
Access keys — the most dangerous credential a user holds #
An access key is a two-line string.
Access Key ID: AKIAIOSFODNN7EXAMPLE
Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEYAnyone who has these two values can exercise all of that user’s permissions. So follow these rules.
- A user has at most 2 access keys (two coexist briefly during rotation).
- Never put them in git or code (the root-key-exposure case in Chapter 1).
- Rotate every 90 days (covered in detail in Chapter 6 security basics).
- When possible, use a Role instead of a user access key.
Group — the unit of user management #
A group is a unit that bundles users. Attach a policy to a group and it applies automatically to all users in that group.
Admins → AdministratorAccess
Developers → PowerUserAccess + (IAM deny policy)
ReadOnly → ReadOnlyAccess
Billing → billing-console access onlyWhy attach to a group instead of directly to a user #
Suppose a new developer joins.
In the direct-to-user model, you pick one existing developer and follow their list of policies to attach. Then “what was that policy I added a week ago for that person?” creates omissions, and over time each developer’s permissions diverge subtly.
In the group model, you just add them to the Developers group and you’re done. Permission changes are applied consistently to everyone by changing only the group’s policy, and auditing and review are clean at the group level.
The rule is this: for IAM users, forbid inline policies and directly attached policies, and grant all permissions through groups or roles.
Role — a credential borrowed temporarily #
A Role isn’t anyone’s credential; it’s a credential that anything can borrow temporarily. It has no permanent password or access key. Instead, when someone or something makes AssumeRole, AWS issues a temporary credential valid for 1 ~ 12 hours.
This is the most important point in IAM. It’s hard at first, but once you get used to it, the real power of IAM appears.
When Roles are actually used #
| Scenario | Who calls AssumeRole |
|---|---|
| EC2 accessing S3 | The EC2 instance (automatically) |
| Lambda writing to DynamoDB | Lambda (automatically) |
| An ECS Task calling another service | The Task (automatically) |
| A user in another AWS account accessing some resources of this account | That user, explicitly |
| GitHub Actions pushing to ECR | A GitHub Actions Workflow via OIDC |
| Temporary privilege escalation (break-glass) | A user, explicitly |
In environments like EC2 / Lambda / ECS, AWS automatically calls AssumeRole and feeds the credential to the instance. The code just calls the SDK. There’s no need to handle access keys. This is the core.
A role’s two policies #
A role has two kinds of policy.
1) Trust Policy — decides who can borrow this role.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}The Principal item is who goes in — a service, another account, a user, an OIDC provider, etc.
2) Permission Policy — decides what you can do after borrowing it.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}EC2 instance profile flow #
[EC2 instance]
│
│ 1) request credentials from instance metadata (169.254.169.254)
▼
[IAM]
│ 2) issue temporary credentials of the role attached to the instance
▼
[SDK inside EC2]
│ 3) call S3 with the credentials
▼
[S3]The code inside EC2 doesn’t know the access key. boto3 or aws-sdk pulls the credential from the metadata and uses it on its own. It fundamentally reduces key-exposure incidents.
Managed policies vs inline policies #
Policies are also split by how they’re made.
| Kind | What | Recommendation |
|---|---|---|
| AWS managed | Made by AWS (e.g., AdministratorAccess, ReadOnlyAccess) | Beginner / standard roles |
| Customer managed | Policies I make. Reusable across multiple users / roles | The operational standard — recommended |
| Inline | Granted directly to only one user / group / role | Not recommended — not reusable, hard to track |
Commonly used AWS managed policies #
| Name | What |
|---|---|
AdministratorAccess | Every permission. Dangerous, next to root |
PowerUserAccess | Almost every permission except IAM |
ReadOnlyAccess | Read-only across all services |
Billing | Billing console |
<Service>FullAccess (e.g., AmazonS3FullAccess) | All of one service |
<Service>ReadOnlyAccess | Read of one service |
The common flow is to start with managed policies and gradually narrow, moving to customer-managed.
Operational permission design — patterns that work #
The skeleton of permission design that holds up even in a small team.
1) 4 groups + 3 roles #
| Unit | Name | What |
|---|---|---|
| Group | Admins | AdministratorAccess (MFA enforced) |
| Group | Developers | PowerUserAccess + IAM deny |
| Group | ReadOnly | ReadOnlyAccess |
| Group | Billing | Billing / cost |
| Role | EC2-AppRole | The role the app uses (S3, RDS, etc.) |
| Role | Lambda-WorkerRole | For Lambda |
| Role | CICDRole | Borrowed by GitHub Actions / CodeBuild |
2) Permission Boundary #
A shield that decides “whatever this IAM user does, only within this limit.” It blocks the incident of a junior developer creating a new policy to raise their own permissions.
Developers group → PowerUserAccess
↓ and give every user a permission boundary
Permission boundary → "DevOnly" — can only create dev/* resources3) Enforce MFA #
Enforce it on root and all IAM users. The shape of the enforcement policy is covered in detail in Chapter 6 security basics.
4) Separate users ↔ roles #
Users are only people; machines are all roles. When this is clear, access-key incidents almost never happen.
5) Guardrails with conditions #
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringEquals": { "aws:ResourceTag/env": "prod" }
}
}Use tags consistently, and a single policy line separates environments (the same foundation as the tag strategy in Chapter 3 cost management).
Policy Simulator — a verification tool #
The IAM Policy Simulator (policysim.aws.amazon.com) is a tool that simulates “is this action possible?” for a user / group / role. Use it to confirm a policy change did what you intended.
1) Select a user or role
2) Select the action to simulate (e.g., s3:DeleteObject)
3) Enter the resource ARN (optional)
4) "Run Simulation" → Allowed / Denied and which policy influenced itThe IAM Access Analyzer in Chapter 6 security basics also helps with checking.
Creating an IAM user — hands-on #
Now that you’re here, we recommend creating one IAM user yourself. From Chapter 4 CLI and SDK on, this user and role are needed.
1. Console → IAM → Users → Add users
2. User name: your name (e.g., curtis)
3. Console access: enable, auto-generate password
4. Attach policies directly: AdministratorAccess (for learning — narrow later)
5. Right after creation → enable MFA immediately (enforcement method in Chapter 6)
6. Access keys: issue just one for CLI
7. Download the .csv — the Secret is shown only on this screenRight after this, move on to Chapter 3 cost management and Chapter 6 security basics.
Common pitfalls #
- Attaching policies directly to users — it looks fine up to 10 people, but at 30 permissions start diverging subtly. On a breach you can’t tell whose permissions reach where. Gather them into groups.
- Overuse of
Resource: "*"— it’s convenient at first, but on a breach the blast radius is infinite. Narrow with ARNs in every role you can. - Inline policies — fast when you slam one in, but a month later you don’t know who added it and it isn’t reusable. Move to named, version-managed customer-managed policies.
- Granting access keys directly inside EC2 — code that solves EC2 → S3 credentials with an access key should move to an instance profile (role). Forget the key rotation and an incident lies dormant forever.
- A “test user” left as-is — a former employee’s access key, a bot user created once and forgotten, all remain. Make an auto-disable / delete policy for users unused for 30 / 90 days, and check at a glance with the IAM Credential Report.
- An access key on the root user — the most common incident in Chapter 1. Root gets console and MFA only; never create an access key.
Exercises #
- Based on §“Allow / Deny evaluation order,” write a JSON fragment of the Statement you’d add to make a user with
AdministratorAccessattached still unable to delete objects in a specific bucket. - In §“A role’s two policies,” write one sentence each on what the Trust Policy and Permission Policy decide, and connect which of these flows the
role_arn+source_profileprofile in Chapter 4 CLI and SDK reproduces in the CLI. - Assume a small team you’ll run and rewrite the §“4 groups + 3 roles” table to fit your environment. Pick one machine role from it and note how that role keeps the “users = people, machines = roles” principle from Chapter 6 security basics.
In short: IAM consists of four elements — users, groups, roles, and policies — and a policy is the permission. A policy is JSON of
Effect+Action+Resource(+Condition), and Deny beats Allow. Bundle people into groups and let machines receive credentials via roles, and not putting keys in code is the basis of preventing incidents.
Next chapter #
Now that you have an IAM user, billing comes next. The biggest incident in the first few days is sleeping resources inflating the invoice. In the next Chapter 3 cost management, we sort out the billing alerts and free-tier limit monitoring you need to set up right after sign-up, plus operational-stage Cost Explorer and a tag strategy.