Post

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.

Day 1: Demystifying AWS IAM - Roles, Policies, and Least Privilege

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 AdministratorAccess or throw Action: "*" 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:

  1. 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.
  2. 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.
  3. 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).

IAM Users console overview

  • 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.

IAM Groups console overview

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 Developers group. 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.

IAM Roles console overview

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 .env file 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 like AllowReadSpecificBucketAndObjects.
  • Effect: Either "Allow" or "Deny".
  • Action: The specific AWS API operations. For example, s3:GetObject, ec2:DescribeInstances, or dynamodb: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:

  1. arn:aws:s3:::production-app-data: Points to the bucket container itself. Operations like s3:ListBucket happen at the bucket level.
  2. arn:aws:s3:::production-app-data/*: Points to the objects inside the bucket. Operations like s3:GetObject and s3:PutObject happen 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:

FeatureAWS Managed PolicyCustomer Managed PolicyInline Policy
Created ByAWSYour TeamYour Team
ReusabilityShared across accountsShared across multiple users/rolesStitched to exactly one user/role
VersioningMaintained by AWSUp to 5 versions with rollbackNo version history
Best Use CaseBroad job-function baselinesProduction least-privilege standardsRare, 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?

IAM Policy evaluation logic flow

The decision tree follows three strict rules:

  1. Default Deny (Implicit Deny): By default, all requests are denied. If no policy explicitly allows the action, the answer is NO.
  2. Explicit Allow: If a policy has "Effect": "Allow" matching the principal, action, and resource, access is granted.
  3. 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.

IAM Access Analyzer findings overview

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:

  1. Open IAM in the AWS console and select Policy Simulator (or navigate to policysim.aws.amazon.com).
  2. Select your test user or role.
  3. Choose the target service (e.g. S3) and action (e.g. s3:GetObject).
  4. Enter the simulation resource ARN.
  5. Hit Run Simulation to see an immediate green Allowed or red Denied, 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

  1. Go to IAM -> Roles -> Create role.
  2. Under Trusted entity type, select AWS service, and choose EC2 as the common use case.
  3. Under Add permissions, search for and select your newly created AppReadDemoDataPolicy.
  4. Name the role ec2-demo-s3-reader-role and 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:

  1. Test s3:GetObject on arn:aws:s3:::devsecops-demo-data-bucket/sample.txt: Result is Allowed.
  2. Test s3:DeleteObject on arn:aws:s3:::devsecops-demo-data-bucket/sample.txt: Result is Denied (no permission granted).
  3. 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:

  1. 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.
  2. 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....
  3. Putting Keys on Virtual Machines: Never run aws configure on an EC2 instance or ECS container with permanent keys. Attach an IAM Role with an instance profile instead.
  4. 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/*).

References


You can find me online at:

My signature image

This post is licensed under CC BY 4.0 by the author.