Day 1: Demystifying AWS IAM - Roles, Policies, and Least Privilege
A practical, beginner-friendly walkthrough of AWS Identity and Access Management (IAM) - users, groups, roles, policy anatomy, least privilege, and junior pitfalls.
The Reality of Learning IAM
When you first open the AWS management console, IAM (Identity and Access Management) feels like a maze of JSON documents, cryptic Amazon Resource Names (ARNs), and frustrating red AccessDenied errors.
Whenever a Python script fails to read an S3 bucket or a Lambda function crashes because it cannot write to CloudWatch, the natural beginner instinct is tempting:
“Let me just attach
AdministratorAccessor throwAction: "*"at it so my app works, and I will fix the security later.”
I have felt that exact urge when starting out. But in cloud security and DevSecOps, IAM is the actual perimeter. In the cloud, there is no physical firewall or data center security guard protecting your database. Everything is an API endpoint, and IAM is the single gatekeeper deciding who gets in. If IAM permissions are too loose, an attacker with a single leaked credential owns your entire infrastructure.
Today, we are breaking down IAM from the ground up: the core building blocks, intuitive mental models, how policy evaluation actually works behind the scenes, and the exact beginner traps that bite junior engineers.
The 3 Questions AWS Asks
Every single interaction in AWS (whether clicking a button in the web console, running aws s3 ls in your terminal, or an EC2 instance reading a database) is an authenticated API call.
Before AWS executes that API call, IAM evaluates three fundamental questions:
- Who are you? (Authentication / AuthN): Proving your identity. For a human, this is a username, password, and MFA code. For an application, it is temporary security credentials issued by AWS STS.
- What are you trying to do? (Authorization / AuthZ): Checking whether your identity has explicit permission to run that specific API action on that specific resource.
- Under what context or conditions? (Conditions): Are you logging in from an approved corporate IP? Is MFA enabled on your session? Are you requesting a service in an approved AWS region?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+-------------------------------------------------------------+
| Incoming AWS API Call |
+-------------------------------------------------------------+
|
v
[ 1. AuthN: Who are you? ]
|
v
[ 2. AuthZ: Are you allowed to run this action? ]
|
v
[ 3. Conditions: Does your request meet the rules? ]
|
v
[ Allow OR Deny ]
The Airport Analogy: AuthN vs AuthZ
A classic mental model that helped me keep Authentication and Authorization straight:
- Authentication (AuthN): Showing your passport at airport security. It proves who you are, but having a valid passport does not grant you permission to walk onto any plane on the tarmac.
- Authorization (AuthZ): Showing your boarding pass for seat 14B on flight EK201. Your boarding pass grants you permission to enter that specific plane and sit in that specific seat at that specific departure time.
The Core Building Blocks
IAM is made up of four foundational primitives: Users, Groups, Roles, and Policies.
1. IAM Users (Long-Term Human Identities)
An IAM User represents a single human identity or legacy workload. It has permanent, long-term credentials: a console password and/or access keys (AKIA... access key ID and secret access key).
- When to use: Dedicated human accounts when your team is not yet using AWS IAM Identity Center (SSO).
- Security Rule: Never share user credentials between team members, always enforce Multi-Factor Authentication (MFA), and never create access keys unless strictly required for local CLI access.
2. IAM Groups (Organizational Containers)
A Group is simply a collection of IAM Users. You do not log into a group. Instead, you attach policies to the group, and every member of that group automatically inherits those permissions.
1
2
3
Group: Developers -> Policies: S3ReadOnlyAccess, AmazonEC2ReadOnlyAccess
Group: PlatformOps -> Policies: AmazonVPCFullAccess, CloudWatchFullAccess
Group: SecurityAudit -> Policies: SecurityAudit
- Why use groups: Scalability. If a junior developer joins the team, you add them to the
Developersgroup. You do not spend 20 minutes attaching individual policies to their user account. When they change roles, you remove them from the group.
3. IAM Roles (Temporary Hat / Badge System)
A Role is an identity that does not have long-term passwords or access keys. Instead, a trusted entity (such as an EC2 virtual machine, an AWS Lambda function, an ECS container, or an external user via GitHub Actions) assumes the role.
When an entity assumes a role, the AWS Security Token Service (STS) generates temporary security credentials that expire automatically (typically between 15 minutes and 12 hours).
The Visitor Badge Analogy
Think of an IAM Role like a temporary visitor badge at a secure office building:
- You do not give the visitor a permanent set of master brass keys to every office door.
- Instead, the security desk hands them an RFID visitor badge that only opens the 3rd floor meeting room, and the badge automatically deactivates at 5:00 PM.
- If someone steals the badge at 6:00 PM, it is completely useless.
Why Roles Win Over Users for Applications
If your Python web app running on EC2 needs to upload files to S3:
- The Bad Way: Generating an IAM user access key (
AKIA...), pasting it into an.envfile or environment variable on the server. If that server is compromised or that file is accidentally committed to Git, the key is permanently exposed until someone notices and revokes it. - The DevSecOps Way: Attaching an IAM Role to the EC2 instance profile. The AWS SDK running in your app talks to the local EC2 Instance Metadata Service (IMDSv2) to fetch rotating STS credentials behind the scenes. Zero secrets stored on disk!
Anatomy of an IAM Policy
An IAM Policy is a JSON document that explicitly declares what actions are permitted or denied.
Here is a breakdown of a clean, least-privilege policy allowing read access to a specific S3 bucket:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadSpecificBucketAndObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::production-app-data",
"arn:aws:s3:::production-app-data/*"
]
}
]
}
Deconstructing Each Field
Version: Always set to"2012-10-17". This is the latest policy language version supported by AWS (not the date you wrote the file!).Sid(Statement ID): An optional human-readable label explaining what this block does. Always use descriptive names likeAllowReadSpecificBucketAndObjects.Effect: Either"Allow"or"Deny".Action: The specific AWS API operations. For example,s3:GetObject,ec2:DescribeInstances, ordynamodb:PutItem.Resource: The exact Amazon Resource Name (ARN) targeted.Condition: Optional filters (e.g. require MFA, restrict to office IP, require TLS 1.2).
The S3 ARN Beginner Trap (Why Are There Two ARNs?)
Notice in the example above, under Resource, we specified two lines:
1
2
3
4
"Resource": [
"arn:aws:s3:::production-app-data",
"arn:aws:s3:::production-app-data/*"
]
This is one of the most common stumbling blocks for beginners:
arn:aws:s3:::production-app-data: Points to the bucket container itself. Operations likes3:ListBuckethappen at the bucket level.arn:aws:s3:::production-app-data/*: Points to the objects inside the bucket. Operations likes3:GetObjectands3:PutObjecthappen at the object level.
If you only include production-app-data/* and your code tries to run aws s3 ls s3://production-app-data, AWS will return AccessDenied because s3:ListBucket requires permissions on the bucket root!
Managed Policies vs Inline Policies
When attaching policies, AWS gives you choices:
| Feature | AWS Managed Policy | Customer Managed Policy | Inline Policy |
|---|---|---|---|
| Created By | AWS | Your Team | Your Team |
| Reusability | Shared across accounts | Shared across multiple users/roles | Stitched to exactly one user/role |
| Versioning | Maintained by AWS | Up to 5 versions with rollback | No version history |
| Best Use Case | Broad job-function baselines | Production least-privilege standards | Rare, one-off strict 1:1 bindings |
DevSecOps Rule of Thumb: Use AWS Managed Policies (like ViewOnlyAccess) during early prototyping. As your architecture matures, clone and trim them down into Customer Managed Policies stored in your Infrastructure as Code (Terraform) repository. Avoid inline policies because they become invisible to audits and are hard to update at scale.
How AWS Evaluates Policies (Deny Always Wins)
When an API call is made, how does AWS decide whether to let it through?
The decision tree follows three strict rules:
- Default Deny (Implicit Deny): By default, all requests are denied. If no policy explicitly allows the action, the answer is NO.
- Explicit Allow: If a policy has
"Effect": "Allow"matching the principal, action, and resource, access is granted. - Explicit Deny: If ANY attached policy (identity policy, resource policy, SCP, or permission boundary) has
"Effect": "Deny", the request is immediately killed. An explicit Deny overrides any number of Allows.
Adding Guardrails with Conditions
Conditions let you add contextual safety checks.
Example 1: Enforcing Multi-Factor Authentication (MFA)
This policy explicitly denies any action if MFA is not present:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllWithoutMFA",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Example 2: Restricting Access to Approved Office IPs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonOfficeIPs",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"203.0.113.0/24"
]
}
}
}
]
}
Verifying with IAM Access Analyzer & Policy Simulator
Writing JSON policies by hand is prone to typos. Instead of testing policies by pushing untested code to production, AWS provides two built-in verification tools:
1. IAM Access Analyzer
Access Analyzer checks resource-based policies (S3 bucket policies, KMS key policies, IAM roles) and alerts you whenever a resource is accessible outside your AWS account or organization.
It acts like an automated security auditor, flagging public S3 buckets and cross-account trust relationships before attackers spot them.
2. IAM Policy Simulator
The Policy Simulator lets you test simulated API calls against real users or roles without making live infrastructure changes:
- Open IAM in the AWS console and select Policy Simulator (or navigate to
policysim.aws.amazon.com). - Select your test user or role.
- Choose the target service (e.g. S3) and action (e.g.
s3:GetObject). - Enter the simulation resource ARN.
- Hit Run Simulation to see an immediate green
Allowedor redDenied, complete with the exact policy statement responsible for the decision.
Hands-on Walkthrough: Least-Privilege EC2 S3 Role
Let’s put this into practice by building a real least-privilege role for an EC2 instance that reads data from a designated bucket.
Step 1: Create the Customer Managed Policy
Navigate to IAM -> Policies -> Create policy. Switch to the JSON editor and enter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAppBucketRead",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::devsecops-demo-data-bucket",
"arn:aws:s3:::devsecops-demo-data-bucket/*"
]
}
]
}
Name the policy AppReadDemoDataPolicy and save it.
Step 2: Create the IAM Role
- Go to IAM -> Roles -> Create role.
- Under Trusted entity type, select AWS service, and choose EC2 as the common use case.
- Under Add permissions, search for and select your newly created
AppReadDemoDataPolicy. - Name the role
ec2-demo-s3-reader-roleand click Create role.
Step 3: Test with Policy Simulator
Open the IAM Policy Simulator, select ec2-demo-s3-reader-role, and run the following tests:
- Test
s3:GetObjectonarn:aws:s3:::devsecops-demo-data-bucket/sample.txt: Result is Allowed. - Test
s3:DeleteObjectonarn:aws:s3:::devsecops-demo-data-bucket/sample.txt: Result is Denied (no permission granted). - Test
ec2:TerminateInstances: Result is Denied (role only has scoped S3 read access).
Junior Pitfalls to Avoid
Here are the biggest mistakes I learned to watch out for:
- The Temporary Wildcard Habit: Writing
"Action": "*"and"Resource": "*"when debugging, promising yourself to tighten it later. In fast-paced teams, “later” often never comes, leaving excessive permissions running in production. - Hardcoded Access Keys in Git: Never push access keys to GitHub or embed them in Docker container images. Use git-secrets, Trufflehog, or pre-commit hooks to block commits containing
AKIA.... - Putting Keys on Virtual Machines: Never run
aws configureon an EC2 instance or ECS container with permanent keys. Attach an IAM Role with an instance profile instead. - Neglecting the Root Account: The AWS root account should have zero access keys, a strong hardware MFA token enabled, and only be used for billing or initial organization setup.
Key Takeaways
- IAM is the primary security boundary in AWS: treat policy design as core application architecture.
- Always prefer temporary credentials (IAM Roles) over long-term secrets (IAM Users).
- Least privilege is an iterative habit: start with zero permissions, grant only what is needed, and tighten using IAM Access Analyzer.
- Explicit Deny always beats Allow: use Deny statements for guardrails like mandatory MFA or IP limits.
- Separate bucket-level permissions (
arn:aws:s3:::bucket) from object-level permissions (arn:aws:s3:::bucket/*).






