AWS IAM — Complete Guide
What is IAM?
IAM (Identity and Access Management) is AWS’s system for controlling who can access what in your AWS account. It is free to use and global — it applies across all AWS regions.
Everything in AWS eventually comes down to IAM: every API call, every service interaction, every CLI command is authorised (or denied) by IAM.
Key Concepts
Users
An IAM User is an identity representing a person or application that interacts with AWS. Users have long-term credentials (password for console, access keys for programmatic access).
- Use for individual people or services that need persistent access
- Avoid sharing users between people
- Disable or delete users that are no longer needed
Groups
A Group is a collection of users. Policies attached to a group apply to all users in it.
- Easier to manage permissions at scale — change the group, not each user
- A user can belong to multiple groups
- Groups cannot be nested (no groups inside groups)
Policies
A Policy is a JSON document that defines what actions are allowed or denied on which resources.
There are two types:
- AWS Managed — pre-built by AWS (e.g.
AmazonEC2ReadOnlyAccess,AmazonEC2FullAccess) - Customer Managed / Inline — custom policies you write yourself
Policy structure:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "HumanReadableIdentifier",
"Effect": "Allow | Deny",
"Action": "service:ActionName",
"Resource": "*"
}
]
}
Roles
A Role is like a user but designed to be assumed temporarily by a service or application, not a person. Roles have no long-term credentials.
- EC2 instances assume roles to access S3, CodeDeploy, etc.
- GitHub Actions assumes a role to deploy to AWS
- Lambda functions use roles to write to DynamoDB
IAM Rules to Remember
Deny always wins
An explicit Deny in any policy overrides any Allow, regardless of how many allow policies are attached.
Default is Deny
If no policy explicitly allows an action, it is denied by default.
Policies are additive
Multiple allow policies stack — a user gets the union of all their allowed actions.
Principle of Least Privilege
Grant only the minimum permissions needed to do the job — nothing more.
Why it matters
- If credentials are compromised, the blast radius is limited
- Reduces risk of accidental damage
- Required by most security compliance standards (SOC2, ISO27001, PCI-DSS)
Why AdministratorAccess on every user is dangerous
AdministratorAccess gives full access to every AWS service. If those credentials leak (e.g. accidentally pushed to GitHub), an attacker gets complete control of the entire AWS account — infrastructure, data, billing, everything.
The rule of thumb
Ask: “What is the minimum this user or service needs to do its job today?” Grant only that.
Workshop Tasks — Step by Step
Task 1 — Read-Only EC2 Access for a Junior Engineer
Goal: Create a user who can view EC2 resources but not modify anything.
- Go to IAM → Users → Create user
- Name:
junior-engineer-<YOUR NAME> - Leave “Provide user access to the AWS Management Console” unchecked
- Click Next
- Select Attach policies directly
- Search for and select
AmazonEC2ReadOnlyAccess - Click Next → Create user
Why AmazonEC2ReadOnlyAccess? It grants only Describe* and List* actions on EC2 — the user can see everything but cannot create, modify, or delete anything.
Task 2 — Full EC2 Access but Block Running Instances
Goal: Upgrade the junior engineer to full EC2 access, but still prevent them from launching new instances.
Step 1 — Swap the policy
- Open
junior-engineer-<YOUR NAME>in IAM - Under Permissions, check
AmazonEC2ReadOnlyAccessand click Remove - Click Add permissions → Attach policies directly
- Search for and attach
AmazonEC2FullAccess
Step 2 — Add a custom deny policy
- Click Add permissions → Create inline policy
- Switch to the JSON editor
- Paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRunInstances",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "*"
}
]
}
- Name it
DenyRunInstancesand save
Why does Deny override Allow? In IAM, an explicit Deny always wins. Even though AmazonEC2FullAccess allows RunInstances, the inline deny policy blocks it.
Tip: When swapping policies, add the new one first before removing the old one — this avoids a window where the user has no permissions at all.
Task 3 — Deny Terminate Instances on Your Own User
Goal: Attach a custom deny policy to your own user so you can launch instances but not terminate them.
- Go to IAM → Users and click your own username
- Click Add permissions → Create inline policy
- Switch to JSON and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyTerminateInstances",
"Effect": "Deny",
"Action": "ec2:TerminateInstances",
"Resource": "*"
}
]
}
- Name it
DenyTerminateInstancesand save
To verify it worked: Go to EC2, launch a new instance (should succeed), then try to terminate it — you should get: Failed to terminate an instance: You are not authorised to perform this operation.
Note: IAM policy names cannot be renamed after creation. If you need to correct a name, delete the policy and recreate it.
Task 4 — CI-CD User with Minimal EC2 Permissions
Goal: Create a dedicated user for a CI-CD script that can only perform the three actions it needs.
Step 1 — Create the policy first
- Go to IAM → Policies → Create policy
- Switch to JSON and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CICDMinimalEC2Access",
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:CreateImage",
"ec2:TerminateInstances"
],
"Resource": "*"
}
]
}
- Name it
CICD-EC2-MinimalAccessand create it
Step 2 — Create the user
- Go to IAM → Users → Create user
- Name:
cicd-deploy-<YOUR NAME> - Leave console access unchecked — this user only needs programmatic (API/CLI) access
- Click Next → Attach policies directly
- Search for
CICD-EC2-MinimalAccess(click the refresh icon if it doesn’t appear immediately) - Attach it and create the user
Tip: If a newly created policy doesn’t appear in the search list, click the refresh icon next to “Create policy” to reload the list.
Why is this better than giving full EC2 access? The CI-CD script only needs three actions. Giving it AmazonEC2FullAccess would mean a compromised CI-CD token could delete security groups, modify network ACLs, or access all instances — far beyond what the script needs.
Common AWS Managed Policies for EC2
| Policy | What it allows |
|---|---|
AmazonEC2ReadOnlyAccess | View/list EC2 resources only |
AmazonEC2FullAccess | Full access to all EC2 actions |
For a full list of EC2 permissions: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html
Tips and Gotchas
- Inline vs Managed policies — Inline policies live on a single user/role and are deleted with it. Managed policies are standalone and can be reused across multiple users.
- Policy names are permanent — you cannot rename a policy. Delete and recreate if you need a different name.
- Newly created policies may not appear immediately in the attach policy search — hit the refresh icon.
- Groups are best practice — attaching policies to groups rather than individual users is easier to manage at scale.
- Roles over users for services — always use IAM roles for EC2, Lambda, and GitHub Actions rather than embedding user credentials.
- Never commit access keys to Git — store them in GitHub Secrets, AWS Secrets Manager, or environment variables.
- AdministratorAccess is a red flag — if every user in your account has it, your security posture is weak.