Last updated: April 29, 2026
AWS IAM is the single largest source of cloud misconfigurations. It’s also AWS’s most powerful feature. Master it and you can architect least-privilege cleanly; fumble it and you ship the kind of blast radius that makes every new access key a production-impacting event.
This module is the concrete IAM practitioner’s guide. You’ve seen the mental models in Module 1. Now we write actual policies, understand how AWS evaluates them, and cover the patterns that Indian fintech auditors, SOC 2 assessors, and your pen-testers actually look for.
The four core objects
- User — a named identity (typically human). Has long-lived credentials. Use sparingly; prefer SSO-federated access
- Group — a collection of users. Attach policies here; users inherit via membership
- Role — an assumable identity. No long-lived credentials; temporary credentials via STS. Used by workloads (EC2, Lambda) and humans (via AssumeRole)
- Policy — a JSON document describing permissions. Attached to users, groups, or roles (identity-based), or directly to resources (resource-based)
Policy anatomy
An IAM policy is a JSON document with one or more Statement blocks:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadFromSpecificBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::ringsafe-app-data",
"arn:aws:s3:::ringsafe-app-data/*"
],
"Condition": {
"StringEquals": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
]
}
Five key elements per statement:
- Effect — Allow or Deny
- Action — one or more service:API calls
- Resource — ARNs affected
- Condition — optional constraints (IP range, MFA presence, time of day, tags, etc.)
- Principal — only in resource-based policies, defines who the policy applies to
Policy evaluation logic
When a request arrives, AWS evaluates multiple policies in a specific order:
- Organizational Service Control Policies (SCPs) — guardrails at the org level. If an SCP denies, the request is denied regardless of any other policy
- Identity-based policies attached to the requesting principal
- Resource-based policies attached to the target resource
- Permission boundaries — maximum permissions a principal can have
- Session policies — passed at AssumeRole time
The logic:
- Explicit Deny wins. If any applicable policy says Deny, the request is denied
- Explicit Allow required. For the request to succeed, at least one applicable policy must explicitly Allow. Default is Deny
- SCPs and permission boundaries cap maximum permissions; they don’t grant
Most real-world confusion: “I have AdministratorAccess but can’t access this bucket.” Likely because an SCP at the org level denies, or the bucket’s resource policy doesn’t include the caller’s account.
Patterns you should know
Role assumption flow
A developer wants temporary admin access. Instead of giving them AdministratorAccess on their user, they assume a role:
# Role trust policy — who can assume
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:user/priya"},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {"aws:MultiFactorAuthPresent": "true"},
"NumericLessThan": {"aws:MultiFactorAuthAge": "3600"}
}
}]
}
# Role's permission policy — what the role can do
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}
Priya has no permanent admin. To get admin, she must assume the role with MFA within the last hour. Session is 1-hour by default. This shrinks blast radius — a stolen user credential alone doesn’t grant admin without MFA.
Workload identity for EC2 / EKS / Lambda
Never put access keys on a running workload. Use the service’s native role attachment:
- EC2 — Instance Profile; app reads temp creds from the instance metadata service (use IMDSv2, never IMDSv1)
- Lambda — Execution Role; creds injected into the function’s environment at runtime
- EKS (Kubernetes) — IAM Roles for Service Accounts (IRSA); service account annotated with role ARN, pod auto-receives creds
- ECS — Task Role (similar to EC2 instance profile, scoped to task)
Least-privilege for common workloads
A common requirement: a Lambda function that reads from one DynamoDB table and writes to one S3 bucket. The policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:ap-south-1:111111111111:table/ringsafe-users"
},
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::ringsafe-exports/*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-south-1:111111111111:log-group:/aws/lambda/ringsafe-export:*"
}
]
}
Compare to the common anti-pattern:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["dynamodb:*", "s3:*", "logs:*"],
"Resource": "*"
}]
}
Both “work.” The first makes a compromise a minor incident; the second makes it a catastrophe. When a pen-tester discovers the Lambda’s role, the first gives them one table and one bucket to poke; the second gives them the entire DynamoDB and S3 surface.
Conditions — the refinement layer
IAM Conditions let you enforce fine-grained constraints:
| Condition | Use |
|---|---|
aws:MultiFactorAuthPresent |
Require MFA on sensitive actions |
aws:SourceIp |
Restrict to your VPN / office IP range |
aws:SourceVpc / aws:SourceVpce |
Restrict to requests from a specific VPC / VPC endpoint |
aws:ResourceTag / aws:PrincipalTag |
Attribute-based access control (ABAC) — match principal tag to resource tag |
aws:RequestTag |
Require specific tags on newly-created resources |
aws:SecureTransport |
Require HTTPS (TLS) |
aws:CurrentTime |
Time-bound permissions |
aws:userid |
Principal-specific matching |
Real example — restrict S3 access to your VPC only (prevents data exfil even with compromised credentials):
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::sensitive-bucket",
"arn:aws:s3:::sensitive-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0abc1234def5678"
}
}
}
Attached to the bucket as resource policy: any request not coming through the specific VPC endpoint is denied, even if the caller has s3:* via identity policy.
The patterns auditors flag in Indian fintech
From engagements across RBI-regulated fintechs in 2024-25, the most common findings:
- Users with long-lived access keys older than 90 days — audit finding even if rarely used
- Roles with
*in Action or Resource — especially if the role is assumed by cross-account principals - AdministratorAccess attached to named users rather than SSO-federated — admin rights without MFA or session bounds
- Permission boundaries not applied to privileged roles — nothing caps what Admin can do
- No SCPs at organisation level — account compromise can create new high-privilege resources unimpeded
- EC2 instance profiles with broader permissions than the running workload needs — Instance Metadata API abuse surface
- IAM policies without Condition blocks — permissions granted with no context checks
- Cross-account role trust policies listing entire accounts rather than specific principals
- IMDSv1 still enabled — server-side request forgery (SSRF) in an app can steal instance credentials
- No logging or alerting on privilege escalation indicators (creation of AdministratorAccess policies, role assumption by unexpected principals)
Privilege escalation paths in IAM
The Rhino Security Labs research on IAM privilege escalation is canonical. Roughly 20+ classified paths. The common ones:
- iam:CreateAccessKey on another user — attacker makes new key for target user, then acts as target
- iam:AttachUserPolicy / AttachRolePolicy — attach AdministratorAccess to self
- iam:PutUserPolicy / PutRolePolicy — embed inline admin policy
- iam:PassRole + lambda:CreateFunction — create a Lambda with a privileged role attached, invoke, escalate
- iam:PassRole + iam:UpdateAssumeRolePolicy — modify role trust policy to allow self
- sts:AssumeRole on cross-account role with broad trust — if the trust policy trusts a whole account, anyone in that account can assume
Tools like Pacu and PMapper automate discovery of these paths. Run them against your own accounts monthly.
SSO federation — the better default for humans
For human users, long-lived IAM users are increasingly an anti-pattern. Better:
- Centralise identity in an IdP (Okta, Azure AD, Google Workspace, AWS SSO/IAM Identity Center)
- Federation maps IdP groups → AWS roles
- Humans assume roles via the IdP — no long-lived AWS credentials ever exist
- Temporary credentials rotate every 1-12 hours via session length
- MFA enforced at the IdP; no extra AWS-side MFA required
- Offboarding is a single IdP action — all AWS access revoked automatically
IAM Identity Center (formerly AWS SSO) is AWS’s managed service for this. For orgs with an existing IdP, federate into IAM Identity Center; for standalone, use it directly with its built-in user store.
Per-service cheat sheet
Common IAM gotchas in specific services:
- S3 — Bucket Policy (resource-based) interacts with IAM policy (identity-based). Public access is often bucket-level. Enable Account-Level Block Public Access
- KMS — Key Policy is required (not just IAM policy). A key with empty key policy is unusable even by the account root
- Lambda — Execution role needs PassRole permission on itself for some actions; function’s own permissions are different from who can invoke
- EC2 — Instance Profile ≠ Role; they’re distinct objects that reference each other. Deletion order matters
- RDS — IAM database authentication available but often overlooked; use it
- Secrets Manager / SSM Parameter Store — policies on the secret itself control who can retrieve; in addition to IAM on the caller
Hardening checklist
- Root account: MFA, no access keys, unused for daily operations, credentials in a vault
- All human users: SSO-federated or at minimum MFA-required
- All workloads: IAM roles, not long-lived keys
- Access Advisor: review quarterly, trim unused permissions
- SCPs at organisation level: deny root account key creation, deny SCP modification, deny region egress outside your scope
- Permission boundaries on privileged roles
- CloudTrail: enabled everywhere, logs to separate security account, integrity validation on
- GuardDuty: enabled in every region of every account
- IAM Access Analyzer: enabled at org level; surfaces unintended external access
- Regular Pacu / PMapper runs against own account (bi-annually at minimum)
Quick reference summary
- IAM objects: User, Group, Role, Policy
- Policy elements: Effect, Action, Resource, Condition, Principal (resource-based only)
- Evaluation: SCP → Identity → Resource → Permission Boundary → Session. Explicit Deny wins; Explicit Allow required
- Workload identities (roles) over long-lived keys — always
- Least privilege in policies: specific Action + specific Resource; avoid
*:* - Conditions are the refinement layer: MFA, source IP, VPC, tags, time, TLS
- Audit findings in Indian fintech: stale keys,
*permissions, no SCPs, no permission boundaries, IMDSv1 - Privilege-escalation paths: CreateAccessKey, AttachPolicy, PutPolicy, PassRole + Lambda, AssumeRole on broad trust
- SSO federation for humans; IAM Identity Center is the managed option
- Hardening: MFA root + workload roles + SCPs + permission boundaries + CloudTrail + GuardDuty + Access Analyzer + quarterly review
Take the quiz. Next: S3 Security & Misconfigurations — where we dig into the single service that has caused more publicly-disclosed data breaches than any other.
Module Quiz · 20 questions
Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.
Real-World Case Study: Code Spaces, 2014
The story. Code Spaces was a UK code-hosting startup competing with GitHub. Profitable. Real customers. On 17 June 2014, the company died in 12 hours.
The technical chain.
- An attacker acquired the AWS root account credentials. (Method never disclosed — phishing the founder is the most likely theory.)
- Attacker logged into the AWS Console at 09:00.
- Attacker created several new IAM users with admin rights and demanded ransom via Code Spaces’ Twitter.
- Code Spaces’ founder logged in at 11:00 and revoked the rogue IAM users.
- Attacker — still holding root — saw this. Began deletion as retaliation.
- Within an hour: every EC2 instance, every EBS volume, every S3 bucket, every AMI, every RDS instance, every off-site backup (which, fatally, was also in AWS) — gone.
By 21:00 the company posted: “Code Spaces will not be able to operate beyond this point. The cost of resolving this issue to date and the cost of refunding customers who have data on the cloud, who do not wish to operate with us as a company any longer, has put Code Spaces in an irreversible position.”
What IAM controls would have prevented this.
- MFA on root. Hardware token. Locked in a safe. Used only for billing and for emergencies that can’t be handled by IAM Identity Center.
- Service Control Policies (SCPs) at the AWS Organization level — even root cannot delete certain resources without breaking the SCP first (which itself requires a 24-hour delay if configured).
- S3 Object Lock in compliance mode on backup buckets — once written, the data cannot be deleted by anyone, including root, until the retention period expires.
- Cross-account backup vault — backups in a different AWS account whose IAM is not reachable from production credentials. Geographically separate region.
- CloudTrail to a separate account with deny-delete on the trail itself. So even an attacker can’t cover their tracks.
The takeaway. AWS root credentials are nuclear. Treat them as such. The blast radius of a compromised IAM-not-root account is whatever IAM Identity Center scoped them to. The blast radius of a compromised root, with no SCPs and no off-account backups, is your company. Build for the latter scenario.
Custom team training + practitioner advisory
Beyond the free academy — we run private workshops, vCISO advisory, and red-team exercises tailored to your stack. For Indian SMBs scaling past their first hire.