Notes · Learn AWS · CHAPTER 2

IAM & Identity

The chapter every later chapter assumes. Users, roles, policies, STS - and the single biggest mental shift coming from AAD: roles aren't users with hats, they're identities anyone allowed can wear. If something later breaks with "access denied", odds are this chapter has the answer.

AAD treats users, groups, and service principals as variations on one theme: "an identity with permissions attached". IAM splits that into two fundamentally different things - users (long-lived) and roles (assumed temporarily) - and adds resource-based policies (permissions attached to the resource, not the identity). The model is more flexible, but more pieces means more places for confusion. We'll build the picture one layer at a time.

In this chapter
  1. The cheat table
  2. The IAM mental model
  3. Users vs Roles (the big one)
  4. Identity- vs resource-based policies
  5. Trust policies and AssumeRole
  6. How a request gets evaluated
  7. Permission boundaries
  8. Workload identity preview (IRSA)
  9. Try it: build a role and assume it
  10. Quick check (quiz)
  11. Gotchas for Azure devs
  12. Project Compass: lay the IAM foundation
  13. Recap & next

The cheat table: AAD → IAM

ConceptAzure / AADAWS / IAM
Human userAAD userIAM Identity Center user (preferred) or IAM User (legacy)
GroupAAD security groupIAM Group (only contains users, not roles)
App/workload identityService Principal / Managed IdentityIAM Role (assumed by services)
Long-lived secretSP client secretIAM User access key (avoid)
PermissionsRBAC role (built-in or custom) assigned at a scopeIAM Policy (JSON) attached to identity or resource
Built-in rolesOwner, Contributor, Reader, ~70 othersAWS-managed policies: AdministratorAccess, ReadOnlyAccess, ~700 others
Permission scopeMgmt group / sub / RG / resourceAccount-wide by default; narrowed in the policy itself via Resource ARNs / Conditions
Resource-attached permsrare - mostly Key Vault access policies, storage SASResource-based policies (S3 bucket policy, KMS key policy, SQS, Lambda, etc.) - first-class
FederationAAD is the IDP by defaultExternal IDP (Entra, Okta, Google) feeds Identity Center; or OIDC for workloads
Workload identity for k8sAzure AD Workload Identity (AKS)IRSA - IAM Roles for Service Accounts (EKS)
Audit trailActivity Log + AAD sign-in logsCloudTrail (all API calls, all services)
"Lock down even admins"Azure Policy + management lockSCPs (Service Control Policies) + permission boundaries
The row to internalize: Service Principal → IAM Role. In Azure an SP is an identity with credentials; you authenticate as it. In AWS a role is an identity with no credentials; an authenticated principal assumes it and gets temporary credentials. This single inversion changes how almost every workload pattern is shaped.
What's in a name? - the IAM glossary
IAM
Identity and Access Management. Launched June 2011. Before IAM existed, AWS access was "all or nothing" via the account's master credentials. This is why every old AWS tutorial yells at you about "the root user".
STS
Security Token Service. Mints temporary credentials. Every AssumeRole* API call goes through STS. The service itself is free - you pay only for what the tokens then go and DO.
SCP
Service Control Policy. Org-level guardrail policies. Live at the OU/account boundary in AWS Organizations. They cannot grant anything - only cap. SCPs win over even the root user. We cover them in chapter 12.
IRSA
IAM Roles for Service Accounts. The EKS workload-identity pattern. A pod's Kubernetes ServiceAccount maps to an AWS IAM role via OIDC. Pronounced "ER-suh" by most teams. The newer alternative is "EKS Pod Identity" - same idea, simpler setup.
"Principal"
From Latin principalis, "first in importance". In IAM it means the actor making a request - user, role, federated identity, or AWS service. NOT a synonym for "principle". (The bug-hunt later in this chapter is built on that mistake.)
"Action"
An API operation, namespaced by service: s3:GetObject, ec2:RunInstances. Wildcards work (s3:*) but expand at evaluation time, not at paste time - so a policy you wrote three years ago automatically picks up new actions a service adds. Sometimes good, sometimes very not good.
"Resource"
An ARN or list of ARNs the statement applies to. "*" means "any resource of any type that this action could apply to" - which is a much broader claim than it looks.

The IAM mental model

ELI5: how IAM thinks
Imagine a building where every door has a card reader. The card reader can check two things: "is your name on the access list for THIS door?" (resource-based policy), or "does YOUR badge say you can open doors like this?" (identity-based policy). If either says yes and no policy says no, the door opens. AAD only does the second check ("does your badge say...?"); AWS does both, which is why the model has more moving parts.

Every IAM evaluation answers one question: "Can principal perform action on resource, under these conditions?"

The four nouns of every IAM decision PRINCIPAL who? IAM User / Role / SSO session / service ACTION what? s3:GetObject ec2:RunInstances RESOURCE on what? arn:aws:s3:::my-bucket/* arn:aws:ec2:us-east-1:... CONDITION under what circumstances? aws:SourceIp, aws:MultiFactorAuthPresent Effect: Allow ⇔ (Principal, Action, Resource, Condition) all match { "Effect" : "Allow" , "Action" : "s3:GetObject" , "Resource" : "arn:aws:s3:::reports-bucket/*" , "Condition" : { "Bool" : { "aws:MultiFactorAuthPresent" : "true" } } }
Every IAM policy statement is a structured assertion about these four elements. Once the shape is internalized, reading any AWS policy becomes mechanical.
What about "principal" in the policy? Identity-based policies don't include a Principal field - the principal is implicit (it's whoever the policy is attached to). Resource-based policies do include a Principal field, because the resource needs to specify who's allowed in. We'll see both shortly.

Users vs Roles (the big one)

ELI5: user vs role
A user is a person with a name badge they keep in their pocket all day. A role is a hat hanging on a hook. Anyone whose badge says "you may wear the hat" can grab it, put it on (assume the role), and act with whatever the hat allows - but only for a few hours. Then the hat goes back on the hook. AAD only really has badges; AWS uses hats heavily.

IAM User

Long-lived identity. Has a name (alice), optional password (for console), optional access key (for CLI). Permissions attached directly.

Closest Azure analog: an AAD user with assigned RBAC roles.

Modern AWS guidance: avoid IAM Users for humans. Use Identity Center.

IAM Role

Identity with no credentials of its own. Has a name (EC2-S3-Reader), an "anyone allowed to wear me" rule (trust policy), and a permissions policy. Assumed temporarily via STS.

Closest Azure analog: Managed Identity, but more general - roles can be assumed by users, services, federated identities, or other AWS accounts.

IAM Group

A container for users only. Attach policies to the group, all member users inherit them. Cannot contain roles. Cannot be a principal in a policy.

Closest Azure analog: AAD security group. Same purpose - bulk permission assignment.

Side-by-side: workload identity

"How does my code on a VM/container get credentials to talk to other services?" is where the Azure-AWS gap is most visible.

Azure (Managed Identity)
# 1. Enable managed identity on the VM
az vm identity assign \
  --name myvm \
  --resource-group rg-app

# 2. Grant it permissions
az role assignment create \
  --assignee-object-id <principal-id> \
  --role "Storage Blob Data Reader" \
  --scope "/subscriptions/.../mystg"

# 3. Code on the VM "just works"
#    SDK auto-discovers credentials
#    from IMDS endpoint
AWS (IAM Role + Instance Profile)
# 1. Create a role that EC2 can assume
aws iam create-role \
  --role-name MyAppRole \
  --assume-role-policy-document file://trust.json

# 2. Attach permissions
aws iam attach-role-policy \
  --role-name MyAppRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

# 3. Attach role to instance via instance profile
aws ec2 associate-iam-instance-profile \
  --instance-id i-1234 \
  --iam-instance-profile Name=MyAppRole

# Code "just works" - SDK pulls creds
# from IMDSv2 endpoint http://169.254.169.254

Conceptually identical end-state, but AWS makes the role visible as a thing you can reuse. The same MyAppRole can be assumed by ten different EC2 instances, by Lambda functions, or by your laptop (if you allow it in the trust policy).

Same idea in Terraform

Terraform · azurerm
resource "azurerm_user_assigned_identity" "app" {
  name                = "app-mi"
  resource_group_name = "rg-app"
  location            = "eastus"
}

resource "azurerm_role_assignment" "app_storage" {
  scope                = azurerm_storage_account.s.id
  role_definition_name = "Storage Blob Data Reader"
  principal_id         = azurerm_user_assigned_identity.app.principal_id
}
Terraform · aws
resource "aws_iam_role" "app" {
  name = "app-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "app_s3" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Look at the trust policy on the AWS side. That Principal: { Service: "ec2.amazonaws.com" } clause is saying "EC2 itself is allowed to assume this role on behalf of an instance it owns." This is the explicit mechanism. In Azure you just check a checkbox; in AWS you write the trust contract. More verbose, more flexible.

Identity- vs resource-based policies

This is the concept that has no direct AAD analog. AAD permissions are almost always attached to the identity. AWS lets you attach permissions to either side - and many resources can be granted access without touching the requester's identity at all.

Two policy directions, both can grant access Identity-based User +policy: "I can read s3" S3 bucket no policy ✓ allowed Resource-based User no policy on user S3 bucket policy: "alice may read" ✓ allowed
Both pictures show the same outcome (alice reads the bucket). Left: alice has permission attached. Right: bucket has alice on its guest list. Either is enough for AWS to allow the request.

Identity-based policy: attached to who's asking

policies/reader-policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::reports-bucket",
      "arn:aws:s3:::reports-bucket/*"
    ]
  }]
}

Attached to alice (user) or EC2-S3-Reader (role). It grants alice the ability to read from that specific bucket.

Fun fact Notice "Version": "2012-10-17" at the top of every IAM policy? That's not a versioning system for your policy - it's the schema version of the policy language. AWS has only ever shipped two versions: 2008-10-17 (no variables, no per-resource conditions) and 2012-10-17 (everything we use today). The string has stayed frozen for 13+ years because changing it would break every policy on Earth. Always paste the current value; AWS treats anything else as the older schema.

Resource-based policy: attached to the thing being touched

bucket-policy.json (attached to reports-bucket)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::222222222222:user/alice" },
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::reports-bucket",
      "arn:aws:s3:::reports-bucket/*"
    ]
  }]
}

Note the new Principal field. The bucket itself maintains a list of who's allowed in. This makes cross-account access possible without sharing credentials - account A's bucket can list account B's role as a principal, and IAM resolves it without anyone needing dual identities.

Which services support resource-based policies?

ServiceResource policy supported?Used for
S3 (buckets)✓ yes - bucket policyCross-account access, public read, signed URLs
KMS (keys)✓ yes - key policyRequired! Key policy controls access, not user policies alone
SQS, SNS, EventBridge✓ yesCross-account event delivery
Lambda✓ yes - resource policyAllowing API Gateway, S3 events to invoke the function
Secrets Manager✓ yesCross-account secret sharing
IAM Roles✓ yes - the trust policyWho's allowed to assume the role
EC2, RDS, DynamoDB✗ noIdentity policies only
Closest Azure analog: Key Vault access policies (legacy) or Storage Account SAS tokens. Both are "permissions on the resource", but Azure deprecates them in favor of identity-based RBAC. AWS goes the opposite way - resource policies are first-class and constantly used for cross-account work.
Cost trap: the "least-privilege" KMS-key explosion ~$400+ / month

A team adopts "least privilege" zealously. They provision a separate customer-managed KMS key (CMK) per microservice (say 50 services) so each KMS key policy can list only that service's role. They also enable S3 default-encryption with the per-service CMK on every bucket. Three months later, the KMS bill is several hundred dollars per month. The team is confused - they were following best practice. What's happening?

Click to reveal the trap
Two compounding charges.

1. The flat $1/key/month. 50 CMKs × $1 = $50/month baseline, before any actual usage. Tiny per-key, real in aggregate.

2. KMS API calls. Every S3 read/write of a CMK-encrypted object triggers a KMS Decrypt or GenerateDataKey call. At $0.03 per 10K calls, a service doing 10M S3 ops/month spends $30/service/month on KMS alone. Across 50 services that's $1,500. Even with S3 Bucket Keys caching (which reduces but doesn't eliminate calls), bills routinely land $400-$1,500/month.

Fix: use AWS-managed keys (free, automatic rotation) as the default for at-rest encryption. Reserve CMKs for the narrow cases where you actually need them: explicit key rotation control, cross-account encryption, or compliance requirements. Don't conflate IAM least-privilege (a permissioning concept) with per-service KMS keys (an unnecessary cost multiplier).

If you want the audit benefits of CMKs without the call costs, enable S3 Bucket Keys. It uses one envelope key per bucket per day instead of one per object - usually cuts KMS calls by 99%.

Trust policies and sts:AssumeRole

ELI5: trust policy
If a role is a hat on a hook, the trust policy is the sign next to the hook that says "only alice and the EC2 instances tagged 'web' may wear me." Without that sign, nobody can pick up the hat. With the sign, they can - and when they do, they get a temporary badge (STS token) for a few hours.

Every IAM role has two policies hanging off it:

1. Permissions policy

What can the role do? (Once assumed.) Standard identity-based policy - Allow s3:GetObject on bucket X.

2. Trust policy

Who can assume the role? A resource-based policy attached to the role itself. Lists allowed principals.

Real-world incident Code Spaces: from running business to gone in 12 hours (June 2014) whole company destroyed

Code Spaces was a small source-code-hosting startup, like a competitor to early GitHub. They ran their entire stack on a single AWS account using root credentials for admin access. No MFA on the root user. Their backups lived in the same account.

An attacker phished the root credentials, logged into the AWS console, and began deleting resources. When the Code Spaces team noticed and tried to log in to stop them, the attacker demanded a ransom. The team refused. The attacker deleted every EC2 instance, every S3 bucket, every RDS database, and every backup snapshot - all in the same account, all under the same credentials.

Code Spaces closed within 24 hours. Customers lost their repos. The company never recovered.

Lessons: (1) Root credentials get a hardware MFA key and live in a safe, not on a laptop. (2) Backups live in a different account in a different region with different IAM. (3) Use SCPs at the org level to make destructive actions impossible even with valid credentials. We cover the SCP setup in chapter 12.

Trust policy examples

trust-policy-ec2.json · let EC2 instances assume this role
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
trust-policy-cross-account.json · let another account assume this role
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::111111111111:root" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "shared-secret-for-confused-deputy" },
      "Bool": { "aws:MultiFactorAuthPresent": "true" }
    }
  }]
}
trust-policy-oidc.json · let an OIDC-federated workload (k8s, GitHub Actions) assume this role
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
      }
    }
  }]
}

The assume-role flow

What happens when you assume a role Caller user / role / EC2 / k8s pod AWS STS checks trust policy issues temp creds Role: S3-Reader trust policy + perms policy (no creds of its own) Temp credentials AccessKeyId + Secret + SessionToken (1hr default) 1. AssumeRole 2. validate 3. mint 4. caller now acts as the role (1 hour)
The role itself has no credentials. STS mints temporary credentials each time someone is allowed to assume it. This is why "rotating role credentials" isn't a thing - every session is already fresh.
Bug hunt: why can't this Lambda function read S3?

A teammate set up an IAM role for a Lambda function. The Lambda's execution role has the right permissions policy (allows s3:GetObject). But when the function runs, every S3 call returns AccessDenied. They paste their trust policy below. What's wrong?

role-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principle": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
Click to reveal the bug
Typo: "Principle" should be "Principal".

IAM's JSON parser doesn't reject unknown keys - it ignores them. So the trust policy has no valid Principal, which means nothing can assume the role. Lambda tries, gets denied, and the function runs with no AWS credentials at all. Every API call fails with AccessDenied.

This typo is so common that AWS now warns about it in the IAM console's policy validator - but the API still accepts it silently. Always run aws iam simulate-principal-policy or test the role with aws sts assume-role before declaring victory.

Assume a role from your laptop

terminal
# Either via profile (we saw this in chapter 1):
aws s3 ls --profile prod-deploy   # silently runs sts:AssumeRole

# Or explicitly:
aws sts assume-role \
  --role-arn arn:aws:iam::333333333333:role/DeployerRole \
  --role-session-name "sunilt-cli-session"

# Returns AccessKeyId, SecretAccessKey, SessionToken, Expiration
# Export these as env vars and you're now acting as the role

How a request gets evaluated

Every AWS API call goes through the same decision tree. Memorize it - 90% of "why is access denied?" investigations end here.

IAM policy evaluation order (simplified) API call arrives Any explicit Deny in any policy? DENY (final) yes → SCP (org-level) allows the action? DENY no → no ↓ Identity policy allows? OR Resource policy allows? yes ↓
Simplified: real evaluation also considers permission boundaries and session policies. But the order matters: explicit Deny wins everywhere, then SCPs (if you have an Organization), then the Allow check.
RuleMeans
Default denyIf nothing explicitly allows the action, it's denied. You must add an Allow somewhere.
Explicit deny winsOne Effect: Deny in any applicable policy overrides any number of Allows.
Identity OR resource is enoughFor same-account requests, an Allow in either side is sufficient. Cross-account requests need both.
Permission boundary capsIf a boundary exists on the identity, the effective permissions are the intersection of policy and boundary.
SCPs are hard guardrailsIf your org's SCP denies it, no one in your account can do it - not even the root user.

Permission boundaries

A permission boundary is a cap on what an identity can do, regardless of its attached policies. Common use case: a developer can create IAM roles for their app, but the roles they create can never exceed the boundary's allow list. This is how you safely delegate IAM-creation rights.

Effective permissions = identity policy ∩ permission boundary Identity policy s3:* iam:* ec2:* Boundary policy s3:* lambda:* ec2:Describe* Effective: s3:* ec2:Describe* iam:* and lambda:* are in only one circle, so neither applies.
Boundaries are a ceiling, not a floor. They don't grant anything; they only cap the maximum.
Azure analog: Azure has nothing exactly like this. The closest is Azure Policy denying actions at a scope, but Azure Policy is service-by-service and not as composable. Permission boundaries are uniquely AWS - and uniquely useful for delegating IAM safely.

Workload identity preview (IRSA)

Chapter 10 deep-dives EKS. But it's worth previewing the workload-identity pattern now because it's the cleanest example of how IAM roles + OIDC trust come together.

ELI5: IRSA
Your pod in Kubernetes wants to read from an S3 bucket. You don't want to bake AWS credentials into the container image. So Kubernetes hands the pod a signed "ID card" (a JWT). AWS trusts that ID card because you told it to. The pod uses the ID card to assume a specific IAM role, and now it can read S3 - using temporary credentials that auto-renew. Same idea as Azure Workload Identity for AKS, with different plumbing.
IRSA: pod → IAM role via OIDC Pod ServiceAccount "app-sa" Projected JWT signed by EKS OIDC issuer AWS STS AssumeRoleWithWeb- Identity IAM Role trusts OIDC sub: "ns:default,sa:app-sa" Temp creds → S3, DynamoDB... No long-lived secrets. Tokens rotate automatically. The IAM role is the only durable artifact.
For the full setup including the EKS OIDC provider registration and the ServiceAccount annotation, see Chapter 10.

Try it: build a role and assume it

Lab: create a role and assume it from your laptop $0

Goal: create an IAM role that your current user/SSO session is allowed to assume. Then assume it and verify you got new credentials. End-to-end demo of the AssumeRole flow.

Step 1. Get your current caller ARN (we'll trust this principal in the role).

terminal
aws sts get-caller-identity
# Note the "Arn" value. Example: arn:aws:sts::123456789012:assumed-role/DeveloperAccess/sunilt

Step 2. Write a trust policy that allows that ARN to assume a new role.

trust.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DeveloperAccess_*" },
    "Action": "sts:AssumeRole"
  }]
}

Step 3. Create the role, attach a read-only policy.

terminal
aws iam create-role \
  --role-name PlaygroundReader \
  --assume-role-policy-document file://trust.json

aws iam attach-role-policy \
  --role-name PlaygroundReader \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

Step 4. Assume the role.

terminal
aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/PlaygroundReader \
  --role-session-name lab-session

# Exports the temp creds:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...

# Re-check identity - you're now the role
aws sts get-caller-identity
# Arn: arn:aws:sts::123456789012:assumed-role/PlaygroundReader/lab-session

Step 5. Clean up (no orphan IAM resources).

terminal
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

aws iam detach-role-policy --role-name PlaygroundReader \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
aws iam delete-role --role-name PlaygroundReader

What you learned: roles are not magic. They're identities you can build, trust to whoever you want, give permissions, and step into. The same flow underpins EC2 instance profiles, Lambda execution roles, IRSA, and cross-account access.

Quick check

Test yourself - 5 questions

IAM is the chapter that pays back. Sit with each one before revealing.

1. An IAM Group can contain...

  • users only
  • users and roles
  • roles only
  • users, roles, and other groups
Show answer
Answer: a. IAM Groups contain users only. They can't hold roles or other groups. If you need to give a permission to multiple roles, you attach the same policy to each role - or use SCPs at the org level. This is one of the biggest mental shifts from AAD security groups.

2. To grant a role in account A the ability to read an S3 bucket in account B, what's the minimum you need?

  • Identity policy on the role only.
  • Bucket policy on the S3 bucket only.
  • Both - identity policy on the role AND a bucket policy allowing account A's role.
  • Add account A's role to an IAM Group in account B.
Show answer
Answer: c. Cross-account access requires both sides to agree. The role in A must have an identity policy permitting s3:GetObject on the bucket ARN. The bucket in B must have a bucket policy listing the role's ARN as a Principal. Either side alone is not enough - this is the "both must say yes" rule for cross-account.

3. What's the difference between a trust policy and a resource-based policy?

  • Trust policies use a different JSON schema.
  • Nothing - "trust policy" is just the name for a resource-based policy attached to an IAM role.
  • Trust policies don't support Conditions; resource-based policies do.
  • Trust policies only work for cross-account access.
Show answer
Answer: b. Same JSON shape, same semantics. A trust policy is just the resource-based policy that happens to be attached to an IAM role, controlling who can call sts:AssumeRole on it. The name is historical - AWS uses "trust policy" specifically for roles because it sounds clearer than "the resource-based policy attached to this role".

4. A user has an identity policy that allows "*:*" (everything). You then attach a permission boundary that allows only "s3:Get*". What can the user do?

  • Everything - the identity policy wins.
  • Nothing - the boundary blocks the identity policy.
  • Only s3:Get* actions - effective permissions are the intersection.
  • The user can do s3:Get* plus anything else if MFA is enabled.
Show answer
Answer: c. Permission boundary = ceiling, not a floor. The effective permission set is identity policy boundary. The user can do s3:GetObject, s3:GetObjectVersion, etc - but not s3:PutObject (in identity but not boundary) and not ec2:* (same).

5. An admin runs aws kms decrypt on a KMS key. They have the AWS-managed AdministratorAccess policy attached. The call returns AccessDenied. What's the most likely cause?

  • The key is in a different region.
  • The key's key policy doesn't grant this admin access.
  • The admin needs to assume a role first.
  • KMS requires MFA for all decrypt calls.
Show answer
Answer: b. KMS is the famous exception: even AdministratorAccess doesn't grant access to a key unless the key's own resource policy (the "key policy") allows it. Most other resources fall back to the identity policy if no resource policy exists, but KMS keys always have a key policy and it's authoritative. Region mismatches also happen but they raise NotFoundException, not AccessDenied.

Gotchas for Azure devs

1. Groups can't contain roles. In AAD you can dump SPs into a security group and assign RBAC to the group. In AWS, IAM Groups only contain IAM Users. You can't "group up" roles. If you want similar logic, use a permissions policy that's attached to each role - or use SCPs at the org level.
2. KMS keys MUST have a key policy. Unlike most resources where the user policy is enough, a KMS key won't grant access via identity policy alone. The key's own policy must allow the principal. Many "I have admin, why can't I decrypt?" tickets trace here.
3. Resource-based policies and trust policies are the same shape. Both have a Principal field. The terminology can confuse - a "trust policy" is just a resource-based policy attached to an IAM role specifically. Same JSON, different name.
4. "Principal": { "AWS": "arn:aws:iam::ACCT:root" } doesn't mean "root user". It means "any IAM principal in account ACCT that has an applicable identity policy". The :root there refers to the account root, used as a shorthand for "trust this account's IAM to govern access". Trips up almost everyone.
5. There is no "Reader" role at a scope. In Azure, "Reader at the subscription scope" is a one-click assignment. In AWS, ReadOnlyAccess is an account-wide managed policy; narrowing it requires writing a custom policy with explicit Resource ARNs. Plan to write more JSON than you did in Azure.
6. The aws:PrincipalOrgID condition is your friend for cross-account work. Instead of listing each account ARN in a resource policy, you can say "any principal in my Organization" with one condition. Discover this early; it saves significant policy bloat.

Project Compass: lay the IAM foundation

Picking up from chapter 1, where you set up the compass CLI profile. In this chapter's slice we create the IAM role that gfn-reports will eventually use. The role starts empty - no permissions yet. Future chapters add S3, DynamoDB, SQS, and KMS permissions as those services come online.

Project Compass · Step 2 of 12 Create gfn-reports-role

Why now: gfn-reports will run as a Lambda in chapter 4. Lambdas need an execution role before deployment. We'll prepare that scaffolding now and keep adding permissions chapter by chapter.

This chapter's slice
terminal · create the trust policy
cat > /tmp/compass-trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF
terminal · create and verify the role
# Create the role - no permissions attached yet
aws iam create-role \
  --role-name gfn-reports-role \
  --assume-role-policy-document file:///tmp/compass-trust.json \
  --description "Execution role for the gfn-reports service (Project Compass)" \
  --tags Key=Project,Value=Compass Key=ManagedBy,Value=learn-aws \
  --profile compass

# Attach AWS-managed basic Lambda logging policy
# (chapter 9 will swap this for a tighter custom one)
aws iam attach-role-policy \
  --role-name gfn-reports-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
  --profile compass

# Verify
aws iam get-role --role-name gfn-reports-role --profile compass
aws iam list-attached-role-policies --role-name gfn-reports-role --profile compass

The role now exists. It trusts the Lambda service, can write CloudWatch logs, and is tagged so we can find it later. We deliberately did not attach AdministratorAccess - chapter 3 onward will add only what's needed, when it's needed.

Progress
ch1 · profile ch2 · IAM role ch3 · VPC ch4 · Lambda ch5 · S3 ch6 · DynamoDB ch7 · SQS ch8 · KMS ch9 · alarms ch10 · EKS ch11 · API GW ch12 · Terraform
Don't delete this role. Chapters 3-12 will attach more policies, change the trust policy when we move to EKS, and eventually destroy/recreate it via Terraform. If you want a clean reset later, run aws iam delete-role --role-name gfn-reports-role after detaching all policies - we'll walk that in chapter 12.

Recap & next

What stuck?

The mental model in one sentence

AAD says "the identity has the powers". IAM says "the policy decides, attached either to the asker or the resource, and roles are temporary identities anyone allowed can wear". Once you internalize that switch, the rest is JSON shape memorization.

Common pitfalls so far

TrapFix
"Access denied" on a service I "should" have access toRun get-caller-identity. Check both identity policy and resource policy. Check for explicit Denys.
"My role can't assume another role"Role chaining needs the target trust policy to list your role as a principal, AND your role's identity policy to allow sts:AssumeRole.
"KMS decrypt fails"Check the key policy, not just the user policy. Both must allow.
"My pod can't access S3"You probably need IRSA. Chapter 10 walks the setup.
NEXT CHAPTER
3. Networking (VPC)
VPCs, subnets, security groups (stateful), NACLs (stateless), and why your Azure NSG habits will mislead you.