January 20, 2026

IAM Basics I Keep Coming Back To

IAM identity types, policy evaluation logic, and the fundamentals that matter for AWS security work.

I’m renewing my AWS Security Specialty certification. Wrote SCS-02 a while back, and now SCS-03 covers new services and tooling that didn’t exist then. Rather than grinding practice exams, I’m documenting the material as articles. That forces me to understand it, and maybe helps someone else too.

This part covers IAM fundamentals: identity types, the six policy types, and how AWS evaluates permissions across multiple policies.

AWS IAM Overview

IAM Identity Types

AWS gives you three identity types. Choosing the wrong one is where security problems start.

Users

IAM users are for humans. If you’re creating an IAM user for a Lambda function or an EC2 instance, stop.

The problem with users is credentials. You create a user, you get an access key, and that key doesn’t expire. It sits in ~/.aws/credentials, gets committed to git, ends up in a Docker image. You can guess the rest.

# Finding users who've never logged into the console
aws iam list-users --query 'Users[?PasswordLastUsed==`null`].UserName'

Use IAM users when a human needs console or CLI access from a workstation. Don’t use them when an application, CI/CD pipeline, or AWS service needs access. Use roles for those.

Groups

Groups hold policies inherited by member users, but can’t do anything themselves.

AWS doesn’t support nested groups. The workaround is multiple group memberships:

# Alice needs permissions from both groups - Developers for daily work,
# OnCallResponders for incident access. Policies attached to each group
# combine to form her effective permissions.
aws iam add-user-to-group --user-name alice --group-name Developers
aws iam add-user-to-group --user-name alice --group-name OnCallResponders

Common patterns: Developers (read most things, write to dev), Admins (broad access, still not root), ReadOnly (auditors, new hires), Billing (finance only).

Roles

Roles should be your default choice: no permanent credentials. Something assuming a role gets temporary credentials that expire, so a leak causes time-limited damage.

A role has two parts: a trust policy (who can assume the role) and a permission policy (what the role can do).

# List all roles
aws iam list-roles --query 'Roles[].RoleName'

# See who can assume a specific role
aws iam get-role --role-name MyRole --query 'Role.AssumeRolePolicyDocument'
ScenarioUseReason
Developer console accessUser + MFA + Role assumptionAudit trail, temporary elevation
Lambda functionExecution RoleNo credentials in code
Cross-account accessRoleTrust policy controls access
CI/CD pipelineRole via OIDCNo stored secrets
EC2 workloadInstance Profile + RoleMetadata service provides creds

Policy Types

IAM has six policy types. They interact in ways that aren’t obvious until you understand the evaluation logic.

Identity-Based Policies

These attach to users, groups, or roles, in three flavors.

AWS Managed Policies are pre-built by AWS. Convenient but often too broad. ReadOnlyAccess sounds safe until you realize it includes secretsmanager:GetSecretValue. Always review the full policy document before assigning; the name doesn’t tell the whole story.

Customer Managed Policies are ones you create: version-controlled and reusable across identities. This is what you want for most things.

Inline Policies are embedded directly in a user, group, or role. Hard to audit, and they don’t show up in policy listings. Use sparingly.

# Find inline policies on users
aws iam list-users --query 'Users[].UserName' --output text | \
  xargs -I {} aws iam list-user-policies --user-name {}

Resource-Based Policies

These attach to resources instead of identities: S3 bucket policies, KMS key policies, Lambda resource policies. A Principal element says who the policy applies to.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"AWS": "arn:aws:iam::111111111111:role/DataProcessor"},
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-bucket/*"
  }]
}

Permission Boundaries

Permission boundaries cap the maximum permissions an identity can have, no matter what policies are attached. An identity only gets permissions that exist in both the identity policy AND the boundary. If the identity policy allows s3:* but the boundary only allows s3:GetObject, the effective permission is s3:GetObject.

Use case: developers create IAM roles for their own applications, but shouldn’t be able to create admin roles.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:*", "dynamodb:*", "lambda:*", "logs:*"],
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": ["iam:*", "organizations:*"],
      "Resource": "*"
    }
  ]
}

Even with AdministratorAccess attached, the boundary still blocks IAM actions.

Service Control Policies

Organization-level guardrails. They don’t grant permissions, only restrict what’s possible in member accounts. SCPs don’t affect the management account.

Common patterns: block certain regions, prevent disabling CloudTrail, require encryption.

Session Policies

Passed during AssumeRole to further restrict the session, useful for dynamic permission scoping.

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/DataRole \
  --role-session-name restricted-session \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::specific-bucket/*"}]}'

VPC Endpoint Policies

Controls what can go through a VPC endpoint. Often forgotten when setting up private access to S3 or DynamoDB.

Policy Evaluation Logic

When a request comes in, AWS doesn’t just check if there’s an Allow somewhere. It evaluates policies in a specific order. Understanding that order is critical for debugging access issues.

IAM Policy Evaluation Flow

The AWS documentation has a detailed flowchart, but here’s the sequence:

  1. Deny evaluation: AWS checks all applicable policies for explicit denies first. An explicit deny anywhere stops evaluation immediately.

  2. Organizations SCPs: If the account belongs to an AWS Organization with applicable SCPs, there must be an Allow in the SCP, or it’s an implicit deny.

  3. Resource-based policies: If the resource has a policy (S3 bucket policy, KMS key policy), AWS checks if it allows the action. For same-account requests, an Allow here can grant access even without an identity policy.

  4. Identity-based policies: Does the principal’s identity policy allow the action? No Allow means implicit deny.

  5. IAM permissions boundaries: If the principal has a boundary attached, the boundary must allow the action too. Boundaries only restrict, never expand permissions.

  6. Session policies: For role or federated sessions, a session policy passed during AssumeRole must also allow the action.

Same-Account vs Cross-Account

This distinction trips people up, me included. Here’s the breakdown.

Same account: Either the resource policy OR the identity policy can grant access.

Cross-account: Both the resource policy AND the identity policy must allow access.

Example: Account A has an S3 bucket, and Account B has a role that needs to read from it. The bucket policy must allow Account B’s role, and that role must have s3:GetObject permission. Miss either one and you get AccessDenied.

Implicit vs Explicit Deny

Implicit deny is the default state: no Allow statement exists. Explicit deny means a Deny statement specifically blocks the action.

Explicit deny always wins. You cannot override it with an Allow anywhere else.

{
  "Effect": "Deny",
  "Action": "s3:DeleteBucket",
  "Resource": "*"
}

No matter what other policies say, no one with this policy can delete buckets.

Worked Example

Here’s a scenario where a developer tries to delete an object from S3.

The Setup:

Alice is a developer in the Engineering OU. Her account has several policies in play:

Organization SCP: Restricts all actions to us-east-1 and us-west-2 regions
Identity Policy: AmazonS3FullAccess (allows s3:*)
Permission Boundary: DeveloperBoundary (allows s3:GetObject, s3:PutObject, s3:ListBucket)

Alice runs: aws s3 rm s3://app-data-bucket/old-file.json

AWS walks through each policy type in order:

  1. Explicit Deny Check: AWS scans all applicable policies for any statement that explicitly denies s3:DeleteObject. None exist. Evaluation continues.

  2. SCP Check: The Organization SCP restricts actions to us-east-1 and us-west-2. The bucket is in us-east-1, so the region is allowed, and the SCP doesn’t block S3 actions. Evaluation continues.

  3. Resource-Based Policy Check: The S3 bucket has no bucket policy attached. Nothing to evaluate. Evaluation continues.

  4. Identity-Based Policy Check: Alice has AmazonS3FullAccess attached, which includes "Action": "s3:*" and allows s3:DeleteObject. Evaluation continues.

  5. Permission Boundary Check: Alice’s DeveloperBoundary only allows s3:GetObject, s3:PutObject, and s3:ListBucket. s3:DeleteObject isn’t in that list. DENIED.

Alice gets AccessDenied even though her identity policy grants s3:*. That’s how boundaries are supposed to work: the admin decided developers shouldn’t delete objects, regardless of what policies they accumulate later.

Root Account

Every AWS account has a root user with unrestricted access. IAM policies and SCPs can’t limit it.

Root is needed for: closing the account, changing the account name, certain billing operations, and restoring IAM admin access if you’ve locked yourself out. Everything else should use IAM identities.

MFA is mandatory, and hardware MFA beats virtual. A phone can be compromised, lost, or backed up to iCloud where someone else can access it.

# Check for root activity in CloudTrail
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=root \
  --query 'Events[].{Time:EventTime,Event:EventName,IP:SourceIPAddress}'

Investigate any result beyond initial account setup.

You can restrict root in member accounts with SCPs:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "StringLike": {
        "aws:PrincipalArn": "arn:aws:iam::*:root"
      }
    }
  }]
}

Exam Notes

Policy evaluation order is tested heavily. Know the flowchart. Same-account vs cross-account comes up in scenario questions, so remember cross-account needs both sides to allow. Permission boundaries appear in delegation scenarios. SCPs don’t affect the management account (a common trick question). For root credentials questions, know what only root can do.

Back to blog