AWS IAM Roles & Permissions Series - Detailed Outlines
AWS IAM Roles & Permissions Series - Detailed Outlines
A 4-part series for AWS Security Specialty exam preparation.
Part 1: IAM Foundations - Users, Roles, and Policy Evaluation
---
title: "AWS IAM Foundations: Users, Roles, and How Policies Actually Work"
description: "The mental model every security engineer needs for IAM. Understanding policy evaluation logic changed how I approach AWS security."
pubDate: "2025-XX-XX"
categories: ["cloud-security", "security-engineering", "aws"]
tags: ["AWS", "IAM", "Security", "Roles", "Policies", "Exam Prep"]
series: "AWS IAM Security Deep Dive"
part: 1
toc: true
---Opening Hook (Draft)
The policy said Allow. The role was attached. CloudTrail showed the
AssumeRole worked. AccessDenied anyway.
Spent two hours on a Friday afternoon convinced AWS was broken. Turned
out the S3 bucket had a resource policy I didn't know about. It was
using an allow-list approach—my role ARN wasn't in it.
That's when I realized I didn't actually understand how IAM evaluates
permissions. I knew the pieces. Users, roles, policies. But not how
they fit together when there's more than one policy involved.
This is the mental model I wish someone had given me three years ago.Why this works: Specific (Friday afternoon, S3 bucket), admits ignorance, no dramatic breach story—just a relatable debugging session.
Section 1: IAM Identity Types - Choosing the Right One
Users - When and When Not
- Human access patterns only
- Long-term credentials = long-term risk
- Why service accounts as IAM users is an anti-pattern
- CLI:
aws iam list-users --query 'Users[?PasswordLastUsed==null]'
Groups - The Forgotten Organizer
- Permission inheritance patterns
- Why you can’t nest groups (and workarounds)
- Common group structures (Developers, Admins, ReadOnly, Auditors)
Roles - The Secure Default
- No permanent credentials
- Assumable by users, services, accounts, federated identities
- The trust policy + permission policy duo
- CLI:
aws iam list-roles --query 'Roles[].RoleName'
Decision Framework Table
| Scenario | Use | Why |
|---|---|---|
| Developer console access | User + MFA + Role assumption | Audit trail, temporary elevation |
| Lambda function | Execution Role | No credentials in code |
| Cross-account access | Role | Trust policy controls who |
| CI/CD pipeline | Role (OIDC) | No stored secrets |
| EC2 workload | Instance Profile + Role | Metadata service provides creds |
Section 2: Policy Types - The Six You Must Know
Identity-Based Policies
- AWS Managed (convenience, broad)
- Customer Managed (reusable, version-controlled)
- Inline (coupled to identity, use sparingly)
- Example comparison with same permission three ways
Resource-Based Policies
- S3 bucket policies
- KMS key policies (required for KMS!)
- Lambda resource policies
- SQS/SNS policies
- Trust policies on roles
- Key difference: Principal element required
Permission Boundaries
- The “maximum permissions” ceiling
- Use case: delegating IAM admin safely
- How effective permissions = intersection
- Diagram: Identity policy ∩ Permission boundary = Effective
Service Control Policies (SCPs)
- Organization-level guardrails
- Don’t grant, only restrict
- Common patterns: region restriction, service blocking
- Why SCPs don’t affect management account
Session Policies
- Passed during AssumeRole
- Further restricts session permissions
- Use case: dynamic permission scoping
VPC Endpoint Policies
- Controls access through the endpoint
- Often forgotten attack surface
- Example: restricting S3 endpoint to specific buckets
Section 3: Policy Evaluation Logic (Exam Gold)
The Evaluation Flowchart
Request comes in
↓
Explicit Deny anywhere? → YES → DENIED
↓ NO
SCP allows? (if in Org) → NO → DENIED
↓ YES
Resource-based policy allows? → YES (some cases) → ALLOWED
↓ NO
Identity-based policy allows? → NO → DENIED
↓ YES
Permission boundary allows? → NO → DENIED
↓ YES
Session policy allows? (if applicable) → NO → DENIED
↓ YES
ALLOWEDSame-Account vs Cross-Account Differences
- Same account: Resource OR Identity policy can allow
- Cross-account: Resource AND Identity policy must allow
- This trips up so many people (include example)
Implicit Deny vs Explicit Deny
- Implicit: absence of Allow
- Explicit: Deny statement
- Why explicit Deny always wins
- The “Deny first, then Allow” mental model is wrong
Worked Examples
- User in OU with SCP + managed policy + permission boundary
- Lambda assuming role to access S3 cross-account
- EC2 accessing KMS key (needs key policy!)
Section 4: Root Account - The Nuclear Option
- When root is actually needed (close account, certain billing)
- MFA types: Virtual, Hardware, FIDO (exam asks this)
- Monitoring root usage with CloudTrail
- CLI:
aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=root - SCP to restrict root (except for legit uses)
Section 5: Practical Exercises
Exercise 1: Map Your Current IAM
# Count users, roles, policies
aws iam get-account-summaryExercise 2: Find Over-Privileged Identities
# Users with admin access
aws iam list-attached-user-policies --user-name USERNAMEExercise 3: Test Policy Evaluation
- Use IAM Policy Simulator
- CLI:
aws iam simulate-principal-policy
Exam Callouts Box
- Policy evaluation order is tested heavily
- Know the difference between resource-based and identity-based for cross-account
- Permission boundaries come up in delegation scenarios
- SCPs: remember management account is exempt
Closing
- Teaser for Part 2: STS and temporary credentials
- Key takeaway: IAM isn’t about granting access, it’s about precisely controlling it
Part 2: STS & Temporary Credentials - The Secure Way to Access AWS
---
title: "AWS STS Deep Dive: Temporary Credentials, Federation, and Secure Access Patterns"
description: "Why temporary credentials should be your default. From AssumeRole to SAML federation, here's how secure AWS access actually works."
pubDate: "2025-XX-XX"
categories: ["cloud-security", "security-engineering", "aws"]
tags: ["AWS", "STS", "Federation", "SAML", "OIDC", "AssumeRole", "Exam Prep"]
series: "AWS IAM Security Deep Dive"
part: 2
toc: true
---Opening Hook (Draft)
Got a Slack DM from security at 11pm: "We found AWS keys in a public repo."
Ran `git log` on the commit. Keys were pushed 8 months ago. Created for
a contractor who left the company 6 months before that. Still active.
With PowerUserAccess.
Took us 3 hours to audit what those keys had touched. S3 buckets,
Lambda functions, DynamoDB tables. We couldn't be 100% sure nothing
was exfiltrated.
If that contractor had used a role instead? The credentials would've
expired in an hour. There'd be nothing to leak.
That's when I stopped creating IAM users for anything that wasn't
a human sitting at a keyboard.Why this works: Real timeline (8 months, 6 months, 3 hours), specific services affected, no dramatic “breach” language—just the reality of credential cleanup.
Section 1: Why Temporary Credentials Matter
The Problem with Long-Lived Credentials
- Access keys don’t expire by default
- Rotation is manual and error-prone
- Leaked key = persistent access until discovered
- Your CLI audit command here (reference Part 1)
The STS Solution
- Credentials that expire automatically
- No secrets to store (for roles)
- Built-in audit trail of who assumed what
- Session duration controls
Section 2: The STS API Operations (Exam Favorites)
AssumeRole
- Primary method for role assumption
- Returns: AccessKeyId, SecretAccessKey, SessionToken, Expiration
- Trust policy must allow the caller
- Duration: 1 hour default, up to 12 hours (role setting)
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/TargetRole \
--role-session-name MySessionAssumeRoleWithSAML
- For SAML 2.0 federation (Active Directory, Okta, etc.)
- User authenticates with IdP, gets SAML assertion
- AWS validates assertion, returns temporary credentials
- Duration: up to 12 hours
- Exam: Know the flow diagram
AssumeRoleWithWebIdentity
- For OIDC providers (Cognito, Google, GitHub Actions)
- Modern approach for web/mobile apps
- IRSA uses this under the hood
- Exam: Difference from Cognito identity pools
GetSessionToken
- For MFA-protected API access
- Used with IAM user credentials
- Returns temporary credentials with MFA context
- Exam: When to use vs AssumeRole
GetFederationToken
- For custom federation broker
- More flexible policy passing
- Less common, but exam tests it
- Requires IAM user or root credentials
Comparison Table
| Operation | Use Case | Can Pass Policy? | Max Duration |
|---|---|---|---|
| AssumeRole | Role assumption | Yes (session policy) | 12h |
| AssumeRoleWithSAML | Enterprise SSO | Yes | 12h |
| AssumeRoleWithWebIdentity | Web/Mobile/CI/CD | Yes | 12h |
| GetSessionToken | MFA-protected access | No | 36h |
| GetFederationToken | Custom broker | Yes (required) | 36h |
Section 3: Role Trust Policies - Who Can Assume
Trust Policy Anatomy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-id-from-partner"
}
}
}
]
}Principal Types
- AWS account:
"AWS": "arn:aws:iam::123456789012:root" - Specific role/user:
"AWS": "arn:aws:iam::123456789012:role/RoleName" - AWS service:
"Service": "lambda.amazonaws.com" - Federated:
"Federated": "arn:aws:iam::123456789012:saml-provider/ADFS" - OIDC:
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
The Confused Deputy Problem
- Third-party assumes role on your behalf
- Without protection, any customer of theirs could access your resources
- Solution: External ID in condition
- Exam loves this scenario
Section 4: Cross-Account Access Patterns
Pattern 1: Direct Role Assumption
Account A (Trusting) ←── Account B (Trusted)
Role User/Role
Trust policy allows B Has sts:AssumeRole permissionPattern 2: Role Chaining
User → Role A → Role B → Role C- Maximum 1 hour session when chaining
- Each hop requires trust + permission
- Exam: Know the limitations
Pattern 3: Resource-Based + Role
- Some services allow direct cross-account via resource policy
- S3, KMS, SQS, SNS, Lambda
- When to use which approach
Setting Up Cross-Account Access Step-by-step with CLI commands:
# In trusting account - create role
aws iam create-role --role-name CrossAccountRole \
--assume-role-policy-document file://trust-policy.json
# In trusted account - allow assumption
aws iam attach-user-policy --user-name Developer \
--policy-arn arn:aws:iam::TRUSTED:policy/AssumeRolePolicySection 5: Federation Deep Dive
SAML 2.0 Federation
- Enterprise identity providers (AD FS, Okta, Azure AD)
- Flow: User → IdP → SAML assertion → AWS → Temp credentials
- Setting up SAML provider in AWS
- Attribute mapping (role selection)
OIDC Federation
- Modern alternative to SAML
- GitHub Actions, GitLab CI, Kubernetes
- Creating OIDC provider
- Trust policy with conditions
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}AWS IAM Identity Center (SSO)
- Centralized access management
- Permission sets = IAM policies
- Works with external IdPs
- Exam: Know the difference from direct federation
Cognito Integration
- User Pools (authentication) vs Identity Pools (authorization)
- Authenticated vs unauthenticated access
- Linking to IAM roles
Section 6: MFA-Protected API Access
When MFA Is Required
- Condition:
aws:MultiFactorAuthPresent - Condition:
aws:MultiFactorAuthAge
The GetSessionToken Flow
# Get MFA-protected credentials
aws sts get-session-token \
--serial-number arn:aws:iam::123456789012:mfa/user \
--token-code 123456Policy Example: Require MFA for Sensitive Actions
{
"Effect": "Deny",
"Action": ["iam:*", "organizations:*"],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}Section 7: Session Duration and Limits
Duration Limits by Method
- AssumeRole: 15min to 12h (role’s MaxSessionDuration)
- AssumeRoleWithSAML: 15min to 12h
- AssumeRoleWithWebIdentity: 15min to 12h
- GetSessionToken: 15min to 36h
- GetFederationToken: 15min to 36h
- Role chaining: Maximum 1 hour (hard limit)
Setting MaxSessionDuration
aws iam update-role --role-name MyRole --max-session-duration 43200Practical Exercises
Exercise 1: Set Up Cross-Account Access
- Create role in Account A
- Assume from Account B
- Verify with
aws sts get-caller-identity
Exercise 2: GitHub Actions OIDC
- Create OIDC provider
- Create role with trust policy
- Test from GitHub workflow
Exercise 3: MFA-Protected CLI Access
- Configure MFA device
- Use GetSessionToken
- Verify sensitive actions require MFA
Exam Callouts Box
- Know all 5 STS operations and when to use each
- Role chaining = 1 hour max (tested frequently)
- External ID prevents confused deputy
- SAML vs OIDC vs Cognito differences
- GetSessionToken is for MFA, not role assumption
Closing
- Teaser for Part 3: Advanced patterns (ABAC, conditions, permission boundaries)
- Key takeaway: Temporary credentials should be your default
Part 3: Advanced Permission Patterns - ABAC, Conditions, and Boundaries
---
title: "Advanced AWS IAM Patterns: ABAC, Conditions, Permission Boundaries, and SCPs"
description: "Moving beyond basic policies. How to implement attribute-based access control, bulletproof permission boundaries, and organizational guardrails."
pubDate: "2025-XX-XX"
categories: ["cloud-security", "security-engineering", "aws"]
tags: ["AWS", "IAM", "ABAC", "SCPs", "Permission Boundaries", "Exam Prep"]
series: "AWS IAM Security Deep Dive"
part: 3
toc: true
---Opening Hook (Draft)
I was managing 127 IAM policies across 4 AWS accounts. Every time we
onboarded a new team, I'd spend half a day copying policies, updating
ARNs, and hoping I didn't miss anything.
Then a colleague asked: "Why don't you just use tags?"
Took me a weekend to rewrite everything with ABAC. One policy replaced
23 others. When the Platform team spun up last month, I didn't touch
IAM at all. Tagged their role, tagged their resources. Done.
The advanced IAM patterns aren't complicated. They're just not the
first thing anyone teaches you.Why this works: Specific numbers (127 policies, 4 accounts, 23 replaced), mundane problem (onboarding teams), no vendor-speak—just efficiency gains.
Section 1: IAM Conditions - Precision Access Control
Condition Basics
"Condition": {
"ConditionOperator": {
"ConditionKey": "value"
}
}Common Condition Operators
- StringEquals, StringLike (wildcards)
- ArnEquals, ArnLike
- IpAddress, NotIpAddress
- DateGreaterThan, DateLessThan
- Bool, Null
- ForAllValues, ForAnyValue (for multi-valued keys)
Global Condition Keys (Exam Favorites)
| Key | Use Case |
|---|---|
aws:SourceIp | Restrict by IP/CIDR |
aws:SourceVpc | Restrict to VPC |
aws:SourceVpce | Restrict to VPC endpoint |
aws:PrincipalTag/key | Check caller’s tags |
aws:ResourceTag/key | Check resource tags |
aws:RequestTag/key | Check tags in request |
aws:PrincipalOrgID | Restrict to organization |
aws:PrincipalAccount | Restrict to specific account |
aws:MultiFactorAuthPresent | Require MFA |
aws:MultiFactorAuthAge | MFA recency |
aws:CurrentTime | Time-based access |
aws:SecureTransport | Require HTTPS |
aws:CalledVia | Check calling service |
Service-Specific Condition Keys
- S3:
s3:prefix,s3:x-amz-acl,s3:ExistingObjectTag - EC2:
ec2:ResourceTag,ec2:InstanceType,ec2:Region - DynamoDB:
dynamodb:Attributes,dynamodb:LeadingKeys - KMS:
kms:ViaService,kms:CallerAccount
Real-World Condition Examples
Restrict to corporate IPs:
{
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}Allow only during business hours:
{
"Condition": {
"DateGreaterThan": {"aws:CurrentTime": "2025-01-01T09:00:00Z"},
"DateLessThan": {"aws:CurrentTime": "2025-01-01T17:00:00Z"}
}
}Force encryption on S3 uploads:
{
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}Section 2: ABAC - Attribute-Based Access Control
RBAC vs ABAC
- RBAC: Permissions based on role
- ABAC: Permissions based on attributes (tags)
- Why ABAC scales better
The ABAC Model in AWS
- Principal tags (on users/roles)
- Resource tags (on AWS resources)
- Request tags (in API calls)
- Session tags (passed during AssumeRole)
ABAC Policy Pattern
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Project": "${aws:PrincipalTag/Project}",
"ec2:ResourceTag/Environment": "${aws:PrincipalTag/Environment}"
}
}
}This allows: Users can only manage EC2 instances tagged with their same Project and Environment.
Setting Up ABAC
Tag the role:
aws iam tag-role --role-name DeveloperRole \
--tags Key=Project,Value=Phoenix Key=Environment,Value=devTag the resources:
aws ec2 create-tags --resources i-1234567890abcdef0 \
--tags Key=Project,Value=Phoenix Key=Environment,Value=devSession Tags Pass tags during role assumption:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/MyRole \
--role-session-name MySession \
--tags Key=Department,Value=EngineeringABAC Benefits
- Fewer policies to manage
- Scales with resources automatically
- Self-service within boundaries
- Audit-friendly
Section 3: Permission Boundaries - Delegating Safely
The Delegation Problem
- You want developers to create IAM roles for their apps
- But you don’t want them to create admin roles
- Solution: Permission boundaries
How Boundaries Work
Effective Permissions = Identity Policy ∩ Permission BoundaryCreating a Permission Boundary
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"dynamodb:*",
"lambda:*",
"logs:*"
],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"iam:*",
"organizations:*",
"account:*"
],
"Resource": "*"
}
]
}Applying the Boundary
aws iam put-role-permissions-boundary \
--role-name DeveloperRole \
--permissions-boundary arn:aws:iam::123456789012:policy/DeveloperBoundaryForcing Boundary on Created Roles Require developers to attach boundary to any role they create:
{
"Effect": "Allow",
"Action": ["iam:CreateRole", "iam:AttachRolePolicy"],
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
}
}
}Boundary vs SCP
| Aspect | Permission Boundary | SCP |
|---|---|---|
| Scope | Single identity | Entire account/OU |
| Applied to | Users, Roles | Accounts |
| Grants permissions | No | No |
| Management account exempt | N/A | Yes |
Section 4: Service Control Policies (SCPs)
SCP Fundamentals
- Organization-level guardrails
- Don’t grant, only restrict
- Affect all principals in account (except management account)
- Evaluated before identity and resource policies
SCP Strategy: Deny List vs Allow List
Deny List (block specific actions):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": ["organizations:LeaveOrganization"],
"Resource": "*"
}
]
}Allow List (only permit specific actions):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ec2:*", "s3:*", "lambda:*"],
"Resource": "*"
}
]
}Common SCP Patterns
Region restriction:
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2", "eu-west-1"]
}
}
}Prevent disabling security services:
{
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"securityhub:DisableSecurityHub",
"config:DeleteConfigurationRecorder",
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging"
],
"Resource": "*"
}Require IMDSv2:
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}SCP Inheritance
- Root → OU → Nested OU → Account
- Effective = intersection of all SCPs in path
- Must have explicit Allow at every level (if using allow-list approach)
Section 5: IRSA - IAM Roles for Service Accounts (EKS)
The Problem IRSA Solves
- Pods need AWS access
- Old way: Node role (too broad) or inject credentials (risky)
- IRSA: Per-pod IAM roles via OIDC
How IRSA Works
Pod with ServiceAccount
→ OIDC token injected by EKS
→ Pod calls STS AssumeRoleWithWebIdentity
→ Temporary credentials returned
→ Pod accesses AWS servicesSetting Up IRSA
- Create OIDC provider:
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster --approve- Create IAM role with trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:my-service-account"
}
}
}
]
}- Annotate ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/MyPodRoleIRSA vs Pod Identity
- IRSA: Uses OIDC, more setup, works everywhere
- Pod Identity: Simpler, EKS-managed, newer
- Know both for the exam
Section 6: Resource-Based vs Identity-Based Decision Framework
When to Use Resource-Based Policies
- Cross-account access without role assumption
- Service integrations (Lambda invoke, S3 events)
- Centralized resource with multiple accessors
When to Use Identity-Based Policies
- Same-account access
- Portable permissions (user moves, permissions follow)
- Complex permission logic
Services Supporting Resource-Based Policies
- S3 (bucket policies)
- KMS (key policies - REQUIRED)
- Lambda (resource policies)
- SQS, SNS
- API Gateway
- Secrets Manager
- ECR
- EventBridge
The KMS Special Case
- Key policy is REQUIRED (can’t use identity-only)
- Must explicitly allow account or principals
- Grants as alternative to policy statements
Practical Exercises
Exercise 1: Implement ABAC for EC2
- Tag roles and instances
- Create ABAC policy
- Test access
Exercise 2: Set Up Permission Boundary Delegation
- Create boundary policy
- Allow role creation with boundary
- Verify can’t escalate
Exercise 3: Deploy SCPs
- Create test OU
- Apply region restriction SCP
- Verify enforcement
Exam Callouts Box
- Know global vs service-specific condition keys
- ABAC uses tag-based conditions
- Permission boundary ∩ Identity policy = Effective
- SCPs don’t affect management account
- KMS requires key policy (identity-only doesn’t work)
- IRSA uses AssumeRoleWithWebIdentity
Closing
- Teaser for Part 4: Security, auditing, and misconfigurations
- Key takeaway: Advanced patterns enable scale without sacrificing security
Part 4: IAM Security, Auditing, and Common Misconfigurations
---
title: "IAM Security: Auditing, Misconfigurations, and Privilege Escalation Prevention"
description: "Finding the holes before attackers do. IAM misconfigurations, privilege escalation paths, and how to audit your AWS environment."
pubDate: "2025-XX-XX"
categories: ["cloud-security", "security-engineering", "aws"]
tags: ["AWS", "IAM", "Security Audit", "Privilege Escalation", "Access Analyzer", "Exam Prep"]
series: "AWS IAM Security Deep Dive"
part: 4
toc: true
---Opening Hook (Draft)
Ran IAM Access Analyzer for the first time on an account I'd been managing
for two years. Found 14 resources with public access. Three S3 buckets,
two Lambda functions, and nine IAM roles that could be assumed by external
accounts.
I knew about one of the buckets. The others were news to me.
Spent the next week tracing how each one got that way. Turns out our
CloudFormation templates had been copying the same overly permissive trust
policy for 18 months. Every new microservice inherited it. Nobody noticed
because nothing broke.
That's when I understood something: IAM doesn't fail loudly. It fails by
quietly allowing more than it should. You don't find these problems by
waiting for incidents. You find them by actively looking.
Here's how to look.Why this works: Specific numbers (14 resources, 3 buckets, 2 Lambda, 9 roles, 18 months), admits ignorance (“The others were news to me”), mundane cause (CloudFormation copy-paste), no dramatic breach—just the reality of permission creep.
Section 1: Common IAM Misconfigurations
Overly Permissive Policies
The classic mistakes:
// DON'T DO THIS
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// OR THIS
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}Finding them:
# Find policies with wildcards
aws iam list-policies --scope Local --query 'Policies[].Arn' --output text | \
while read arn; do
aws iam get-policy-version \
--policy-arn "$arn" \
--version-id $(aws iam get-policy --policy-arn "$arn" --query 'Policy.DefaultVersionId' --output text) \
--query 'PolicyVersion.Document' | grep -l '"Action": "\*"' && echo "$arn"
doneOverly Permissive Trust Policies
// DANGEROUS - Any AWS account can assume
{
"Principal": {"AWS": "*"},
"Action": "sts:AssumeRole"
}
// DANGEROUS - Any principal in another account
{
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "sts:AssumeRole"
}Missing MFA Requirements
- Console access without MFA
- Sensitive actions without MFA condition
- Root account without MFA
Unused Credentials
# Users who haven't logged in
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F',' '$5 == "false" {print $1}'
# Access keys not used in 90 days
aws iam list-access-keys --user-name USERNAME
aws iam get-access-key-last-used --access-key-id AKIAXXXXXXXXInline Policies Sprawl
- Hard to audit
- Not version controlled
- Often forgotten
# Find all inline policies
aws iam list-users --query 'Users[].UserName' --output text | \
xargs -I {} sh -c 'aws iam list-user-policies --user-name {} --query "PolicyNames" --output text && echo "User: {}"'Section 2: Privilege Escalation Paths
The Dangerous Permissions
| Permission | Risk | Escalation Path |
|---|---|---|
iam:CreatePolicy | High | Create admin policy |
iam:AttachUserPolicy | High | Attach admin to self |
iam:AttachRolePolicy | High | Attach admin to assumable role |
iam:CreatePolicyVersion | High | Update existing policy |
iam:SetDefaultPolicyVersion | High | Activate malicious version |
iam:PassRole | High | Pass powerful role to service |
iam:CreateAccessKey | Medium | Create keys for other users |
iam:UpdateAssumeRolePolicy | High | Allow self to assume role |
iam:PutUserPolicy | High | Inline policy on self |
iam:PutRolePolicy | High | Inline policy on role |
lambda:CreateFunction + iam:PassRole | High | Execute as any passable role |
ec2:RunInstances + iam:PassRole | High | Launch with any passable role |
sts:AssumeRole | Varies | Depends on assumable roles |
Example Escalation: Lambda + PassRole
# Attacker has: lambda:CreateFunction, lambda:InvokeFunction, iam:PassRole
# 1. Create function with admin role
aws lambda create-function \
--function-name escalate \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler index.handler \
--runtime python3.9 \
--zip-file fileb://malicious.zip
# 2. Invoke function - code runs as AdminRole
aws lambda invoke --function-name escalate response.jsonExample Escalation: CreatePolicyVersion
# Attacker has: iam:CreatePolicyVersion on a policy attached to them
# 1. Create new version with admin access
aws iam create-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/MyPolicy \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' \
--set-as-defaultPreventing Escalation
- Permission boundaries (Part 3)
- Deny statements for dangerous actions
- Monitor IAM changes via CloudTrail
- Regular access reviews
Section 3: IAM Access Analyzer
What Access Analyzer Does
- Finds resources shared externally
- Identifies overly permissive policies
- Validates policies before deployment
- Generates policies from activity
Setting Up Access Analyzer
# Create analyzer
aws accessanalyzer create-analyzer \
--analyzer-name my-analyzer \
--type ACCOUNT
# List findings
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/my-analyzerFinding Types
- External access to S3 buckets
- External access to IAM roles
- External access to KMS keys
- External access to Lambda functions
- External access to SQS queues
- External access to Secrets Manager secrets
Policy Validation
# Validate a policy before applying
aws accessanalyzer validate-policy \
--policy-document file://policy.json \
--policy-type IDENTITY_POLICYPolicy Generation from CloudTrail
# Start policy generation
aws accessanalyzer start-policy-generation \
--policy-generation-details '{
"principalArn": "arn:aws:iam::123456789012:role/MyRole"
}'
# Get generated policy
aws accessanalyzer get-generated-policy \
--job-id JOB_IDSection 4: Auditing IAM with CloudTrail
Critical IAM Events to Monitor
| Event | Risk Level | What to Look For |
|---|---|---|
CreateUser | High | Unexpected new users |
CreateAccessKey | High | Keys for other users |
AttachUserPolicy | High | Admin policy attachments |
AttachRolePolicy | High | Role privilege changes |
CreatePolicyVersion | High | Policy modifications |
UpdateAssumeRolePolicy | High | Trust policy changes |
ConsoleLogin | Medium | Failed attempts, unusual times |
SwitchRole | Medium | Cross-account activity |
AssumeRole | Medium | Role assumption patterns |
CloudTrail Queries
# Find all IAM changes in last 7 days
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=iam.amazonaws.com \
--start-time $(date -d '7 days ago' '+%Y-%m-%d') \
--query 'Events[].{Time:EventTime,Event:EventName,User:Username}'
# Find privilege escalation attempts
for event in CreateUser AttachUserPolicy AttachRolePolicy PutUserPolicy PutRolePolicy CreatePolicyVersion; do
echo "=== $event ==="
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=$event \
--start-time $(date -d '30 days ago' '+%Y-%m-%d') \
--query 'Events[].{Time:EventTime,User:Username,IP:SourceIPAddress}'
doneCloudWatch Alarms for IAM
{
"source": ["aws.iam"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": [
"AttachUserPolicy",
"AttachRolePolicy",
"CreatePolicyVersion",
"PutUserPolicy",
"PutRolePolicy",
"CreateAccessKey"
]
}
}Section 5: IAM Credential Reports and Last Accessed
Credential Report
# Generate report
aws iam generate-credential-report
# Download and analyze
aws iam get-credential-report --query 'Content' --output text | base64 -d > cred-report.csv
# Find users without MFA
cat cred-report.csv | awk -F',' '$4 == "true" && $8 == "false" {print $1}'
# Find old access keys
cat cred-report.csv | awk -F',' '$9 != "N/A" && $9 < "2024-01-01" {print $1, $9}'Last Accessed Information
# Generate service last accessed report
JOB_ID=$(aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::123456789012:role/MyRole \
--query 'JobId' --output text)
# Get results
aws iam get-service-last-accessed-details --job-id $JOB_IDUse this to:
- Identify unused permissions
- Right-size policies
- Find abandoned roles
Section 6: IAM Best Practices Checklist
Identity Management
- MFA enabled for all human users
- MFA enabled for root account (hardware key preferred)
- No access keys for root account
- IAM users only for human access (not workloads)
- Federated access for enterprise (SSO/SAML)
- Regular access key rotation (< 90 days)
- Credential report reviewed monthly
Policies
- No wildcard (*) actions in production policies
- No wildcard (*) resources without conditions
- Customer managed policies (not inline)
- Version control for all policies
- Permission boundaries for delegation
- SCPs for organizational guardrails
Roles
- Roles for all workloads (EC2, Lambda, ECS, etc.)
- Trust policies explicitly scoped
- External ID for third-party access
- Session duration appropriately limited
- IRSA for EKS workloads
Monitoring & Auditing
- CloudTrail enabled in all regions
- CloudTrail logs to central S3 bucket
- Access Analyzer enabled
- Alerts for critical IAM events
- Regular access reviews (quarterly minimum)
- Unused credentials removed
Section 7: Remediation Patterns
Cleaning Up Overprivileged Access
- Generate last accessed report
- Identify unused services
- Create new least-privilege policy
- Test with Policy Simulator
- Apply and monitor
- Iterate
Responding to Compromised Credentials
USER="compromised-user"
# 1. Apply immediate deny policy
aws iam put-user-policy --user-name $USER --policy-name EmergencyDeny \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"*","Resource":"*"}]}'
# 2. Disable all access keys
aws iam list-access-keys --user-name $USER --query 'AccessKeyMetadata[].AccessKeyId' --output text | \
xargs -I {} aws iam update-access-key --user-name $USER --access-key-id {} --status Inactive
# 3. Remove console access
aws iam delete-login-profile --user-name $USER 2>/dev/null
# 4. Investigate via CloudTrail
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=$USER \
--start-time $(date -d '30 days ago' '+%Y-%m-%d')Practical Exercises
Exercise 1: Run a Full IAM Audit
- Generate credential report
- Check for MFA compliance
- Find inactive users
- Review admin access
Exercise 2: Set Up IAM Monitoring
- Create EventBridge rule for IAM changes
- Configure SNS alerts
- Test with policy change
Exercise 3: Identify Escalation Paths
- List all roles with iam:PassRole
- Identify which services they can pass to
- Review trust policies
Exam Callouts Box
- Know privilege escalation patterns (PassRole, CreatePolicyVersion, etc.)
- Access Analyzer: external sharing, policy validation, policy generation
- CloudTrail event names for IAM operations
- Credential report contents and use cases
- Difference between Access Advisor and Access Analyzer
Closing
- Series recap: Foundations → STS → Advanced Patterns → Security
- Final takeaway: IAM is the foundation of AWS security - master it
- Call to action: Audit your own environment this week
Series Summary
| Part | Focus | Exam Weight | Key Topics |
|---|---|---|---|
| 1 | Foundations | High | Users, roles, policy types, evaluation logic |
| 2 | STS & Federation | High | AssumeRole, SAML, OIDC, cross-account |
| 3 | Advanced Patterns | Medium-High | ABAC, conditions, boundaries, SCPs, IRSA |
| 4 | Security & Auditing | Medium | Misconfigs, escalation, Access Analyzer, monitoring |
Cross-References to Your Existing Posts
- AWS CLI Security Manual: Part 4 builds on these commands
- Secrets Management: Part 2 connects to role-based secrets access
- Separation vs Least Privilege: Part 1 & 3 expand on these concepts