IAM & Identity
Table of Contents#
- Why IAM Is the Single Most Important AWS Service
- The Core IAM Vocabulary
- The Root User — Extremely Powerful, Almost Never Used
- IAM Users — Individual Identities
- IAM Groups — Managing Users at Scale
- IAM Roles — The Concept That Actually Matters Most
- Anatomy of an IAM Policy, Line by Line
- Identity-Based vs Resource-Based Policies
- How AWS Actually Evaluates a Permission Request
- The Explicit Deny — Why It Always Wins
- Assuming a Role — The Mechanics
- Cross-Account Access, Worked End to End
- Instance Profiles — How EC2 Gets IAM Credentials Safely
- IRSA — IAM Roles for Kubernetes Service Accounts, Revisited
- Federation and Identity Providers (SSO)
- The Principle of Least Privilege, Applied With Real IAM Tools
- MFA — Multi-Factor Authentication
- IAM Policy Variables and ABAC — Attribute-Based Access Control
- Auditing IAM: Access Analyzer and Credential Reports
- Permission Boundaries — A Ceiling for a Single Identity
- Service-Linked Roles
- Break-Glass Access — Emergency Procedures
- A Deeper Policy Evaluation Example — Deny With a Condition
- Part 2 CLI Cheat Sheet
- IAM Best Practices — The Consolidated Checklist
- A Full Worked Example: Designing IAM for a Three-Tier Application
- Common Mistakes
- Worked Practice Problems
- Summary and What's Next
Why IAM Is the Single Most Important AWS Service#
Every single AWS API call — creating an EC2 instance, reading an S3 object, deleting a database — passes through exactly one gate first: Identity and Access Management (IAM). Every other service in this entire series depends on IAM working correctly; a perfectly configured VPC (Part 4) or a perfectly encrypted S3 bucket (Part 5) is irrelevant if the IAM policy guarding it is wrong. This directly extends the IAM discussion already introduced generically in the DevSecOps series (Part 4) — that tutorial covered IAM as a universal cloud concept; this part covers AWS's specific, and genuinely intricate, implementation of it.
The Core IAM Vocabulary#
Getting this vocabulary exactly right up front prevents a huge amount of later confusion — these terms are used precisely and consistently throughout AWS documentation.
Diagram
| Term | Meaning |
|---|---|
| Principal | The entity making a request — an IAM user, an IAM role, or an AWS service acting on your behalf |
| Authentication | Proving who you are (e.g. a valid access key, a valid password + MFA code) |
| Authorization | Determining what that identity is allowed to do — this is IAM's actual job, evaluated on every single request |
| Policy | A JSON document listing specific Allow/Deny statements for specific actions on specific resources |
| Principle of Least Privilege | Grant only the exact permissions needed, nothing more — already covered generically in the DevSecOps series, now made concrete with real AWS tooling |
The Root User — Extremely Powerful, Almost Never Used#
Every AWS account has exactly one root user, created automatically with the account, tied to the email address used to sign up. It cannot be restricted by any IAM policy or SCP — it can do literally anything, including closing the account and changing billing.
Diagram
Why the standard, universally-agreed practice is to lock the root user away and never use it day-to-day, worth stating precisely: since no policy can restrict it, the root user represents the single largest possible blast radius in the entire account — the standard practice is to set an extremely strong password, enable MFA (covered later in this part) immediately, store the credentials somewhere genuinely secure and rarely accessed, and do essentially ALL actual work through an IAM user or role instead, which — unlike root — CAN be restricted by policy.
IAM Users — Individual Identities#
An IAM user represents a single person or application with long-term credentials (a password for console access, and/or access keys for programmatic access).
# Create an IAM user (console access disabled by default) aws iam create-user --user-name alice # Attach an existing AWS-managed policy to that user aws iam attach-user-policy \ --user-name alice \ --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
Why IAM users are increasingly the LEAST preferred way to grant access, worth stating explicitly and precisely — this is a genuinely important, modern best-practice shift: long-lived access keys attached to an IAM user are a standing, permanent credential that can leak, get committed to a repository (directly connects to the secret-scanning discussion in the DevSecOps series), and simply never expires unless someone manually rotates or revokes it. Modern AWS guidance strongly favors IAM roles (covered next) with short-lived, automatically-expiring credentials wherever possible — reserving IAM users mainly for a small number of genuinely necessary long-lived service accounts, ideally still without static access keys where an alternative exists.
IAM Groups — Managing Users at Scale#
A group is simply a named collection of IAM users, used to attach policies once instead of per-user.
# Create a group and attach a policy to the GROUP, not individual users aws iam create-group --group-name developers aws iam attach-group-policy \ --group-name developers \ --policy-arn arn:aws:iam::aws:policy/PowerUserAccess # Add a user to the group — they now inherit the group's policies aws iam add-user-to-group --group-name developers --user-name alice
A user's effective permissions are the union of everything granted directly to them, plus everything granted through every group they belong to — worth remembering when debugging "why can this user do X" during an access review.
IAM Roles — The Concept That Actually Matters Most#
If this entire part could be reduced to one idea, it would be this one. An IAM role is an identity with NO long-term credentials of its own — instead, a trusted principal "assumes" the role and receives temporary, automatically-expiring credentials.
Diagram
Why roles are considered dramatically safer than long-lived IAM user access keys, worth stating explicitly and precisely: the credentials a role produces are TEMPORARY (typically expiring within an hour by default) and generated fresh on demand — there is no long-lived secret sitting somewhere waiting to leak. A role has exactly two policies attached to it, and it's worth being crystal clear on the difference between them:
| Policy type | What it controls |
|---|---|
| Trust policy | Who is allowed to assume this role at all (e.g. "only EC2 instances," "only account 999988887777," "only this specific GitHub Actions workflow") |
| Permission policy | What the role can actually DO once assumed (e.g. "read from this S3 bucket") |
Anatomy of an IAM Policy, Line by Line#
Worth building the vocabulary to read any real IAM policy confidently, since this JSON shape appears constantly in AWS work.
cat <<'POLICY' { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowReadOnlyOnOneBucket", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::my-app-bucket", "arn:aws:s3:::my-app-bucket/*" ], "Condition": { "StringEquals": { "aws:RequestedRegion": "us-east-1" } } } ] } POLICY
| Field | Meaning |
|---|---|
Version | The policy language version — always "2012-10-17" in practice, a historical artifact worth not overthinking |
Sid | An optional, human-readable statement ID — purely for readability |
Effect | Allow or Deny — the entire point of the statement |
Action | Which specific API operation(s) this statement covers, in service:Action form |
Resource | Which specific resource(s) this statement applies to, as an ARN (Amazon Resource Name) — never leave this as "*" (all resources) unless genuinely intended |
Condition | Optional, additional constraints narrowing WHEN the statement applies (time of day, source IP, MFA presence, requested region, and more) |
Identity-Based vs Resource-Based Policies#
A genuinely important, frequently-tested distinction — WHERE a policy is attached changes what it's called and how it behaves.
Diagram
Why resource-based policies are the mechanism that actually makes cross-account access possible without a role assumption in some cases, worth stating explicitly: an S3 bucket policy (a resource-based policy) can directly name a principal from another AWS account as allowed to read it — the request is authorized if EITHER the requester's identity-based policy allows it, OR the resource's resource-based policy allows it (for most services), which is a genuinely different evaluation logic than identity-based policies alone.
How AWS Actually Evaluates a Permission Request#
A precise, step-by-step mental model worth memorizing exactly — this is a very common interview question, and getting the order slightly wrong gives a wrong answer.
Diagram
The single sentence worth memorizing word for word: "everything is implicitly denied by default; an explicit Allow anywhere applicable grants it; but an explicit Deny anywhere applicable — including an SCP — always wins, no matter how many Allow statements exist elsewhere."
The Explicit Deny — Why It Always Wins#
Worth dwelling on this specific rule with its own concrete example, since it's the piece people most often get wrong when reasoning through a real access problem.
Diagram
Why this asymmetry (Deny always wins, but multiple Allows just combine) is deliberate and important, worth explaining precisely: it lets an organization safely grant a broad, convenient Allow (e.g. "developers get PowerUserAccess") while still carving out hard, un-overridable exceptions with a narrow explicit Deny (e.g. "but never delete anything in this specific backups bucket") — attaching that narrow Deny anywhere in the evaluation chain is guaranteed to win, regardless of how many other broad Allow policies also apply.
Assuming a Role — The Mechanics#
# Assume a role via the CLI, receiving temporary credentials aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/DeploymentRole \ --role-session-name my-deploy-session # The response includes AccessKeyId, SecretAccessKey, and # SessionToken — export them to use the temporary identity export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... export AWS_SESSION_TOKEN=... # Confirm the switch actually worked aws sts get-caller-identity
A role's trust policy is itself just a resource-based policy, attached to the role, specifying exactly who's allowed to call AssumeRole against it:
cat <<'TRUST' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:user/alice" }, "Action": "sts:AssumeRole", "Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } } }] } TRUST
Why requiring MFA in a role's trust policy is a genuinely strong, real-world practice, worth noting: it means even a fully compromised long-lived credential for alice still can't assume this (likely more powerful) role without also passing an MFA challenge — a concrete, layered application of the defense-in-depth idea from the Reliability & Architecture Patterns series' resilience-pattern discussion.
Cross-Account Access, Worked End to End#
Directly building on the multi-account landing zone from Part 1 — this is the actual mechanism that lets a human or automation in one account reach into another, safely and auditably.
Diagram
Why this pattern is dramatically preferable to giving the engineer a separate, permanent IAM user directly in the production account, worth stating explicitly: the engineer never has standing, permanent credentials in the production account at all — access is granted moment-to-moment, expires automatically, is fully visible in CloudTrail (Part 9) as an explicit role-assumption event, and can be revoked instantly by simply changing the trust policy, without needing to hunt down and rotate a credential that might be cached somewhere.
Instance Profiles — How EC2 Gets IAM Credentials Safely#
A concrete, extremely common real-world application of IAM roles: how does code running ON an EC2 instance (Part 3) get AWS credentials, without a developer ever hard-coding an access key into the application?
Diagram
# Attach an instance profile (containing a role) to a running instance aws ec2 associate-iam-instance-profile \ --instance-id i-0123456789abcdef0 \ --iam-instance-profile Name=MyAppRole # From WITHIN the instance, the AWS SDK automatically fetches # temporary credentials from the local metadata service — # no access key ever needs to be stored on disk curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyAppRole
Why this is worth stating as the single strongest reason to always prefer instance profiles over hard-coded credentials on EC2: the credentials are automatically rotated by AWS every few hours, are only ever reachable from within that specific instance's local network namespace, and never need to be written to disk, environment variables, or source control at all — directly reinforcing the "avoid long-lived credentials wherever possible" theme from earlier in this part, and the secrets-management discipline from the DevSecOps series (Part 4).
IRSA — IAM Roles for Kubernetes Service Accounts, Revisited#
Already introduced in the Kubernetes Deep Dive series (Part 5) when discussing EKS specifically — worth revisiting here with the full IAM mental model now in place, since it's genuinely the same role-assumption pattern as everything above, just with a Kubernetes-native trust condition.
Diagram
Why this solves a real, previously-hard problem worth naming precisely: without IRSA, every pod on a node effectively shared whatever broad IAM permissions were attached to the underlying EC2 worker node's instance profile — this is EXACTLY the instance-profile mechanism from the previous section, but applied at the whole-node level rather than per-pod, violating least privilege badly. IRSA lets each individual Kubernetes ServiceAccount assume its OWN, narrowly-scoped IAM role, achieving true per-workload least privilege even though many unrelated pods share the same underlying EC2 node.
Federation and Identity Providers (SSO)#
For real organizations with existing corporate identity systems (Okta, Azure AD, Google Workspace), creating individual IAM users per employee doesn't scale and creates a second identity system to keep in sync. Federation solves this by letting an existing external identity provider (IdP) vouch for who someone is, with AWS issuing temporary role credentials based on that external identity.
Diagram
Why this is the recommended, modern pattern for human access at any real organization, worth stating explicitly: there is exactly ONE place an employee's access gets revoked when they leave — the corporate IdP — instead of needing to separately track down and delete IAM users scattered across dozens of AWS accounts. AWS IAM Identity Center (the successor to the older "AWS SSO" branding) is AWS's own native tool for exactly this, letting a single federated login grant appropriately-scoped role access across an entire AWS Organization.
The Principle of Least Privilege, Applied With Real IAM Tools#
Already covered as a general concept in the DevSecOps series — worth making concrete with the actual AWS tools that help apply it in practice.
# IAM Access Analyzer can GENERATE a least-privilege policy # based on actual CloudTrail activity over a time window — # "what did this role ACTUALLY use, not what it's ALLOWED to use" aws accessanalyzer start-policy-generation \ --policy-generation-details '{"principalArn": "arn:aws:iam::123456789012:role/MyRole"}'
| Technique | What it does |
|---|---|
| Start with AWS-managed policies, then narrow | Faster to get moving, but often broader than needed — treat as a starting point, not a final state |
| Use Access Analyzer's policy generation | Reverse-engineers an actual least-privilege policy from real CloudTrail usage history |
Scope Resource to specific ARNs, never "*" | Prevents a policy intended for one bucket from silently applying to every bucket in the account |
Add Condition blocks (MFA, source IP, time window) | Narrows WHEN a broad permission can actually be exercised, even if the base grant is wide |
MFA — Multi-Factor Authentication#
A simple, foundational control worth stating plainly: every human identity with console access — and especially the root user — should have MFA enabled, no exceptions.
# Enable a virtual MFA device for a user aws iam create-virtual-mfa-device --virtual-mfa-device-name alice-mfa \ --outfile /tmp/QRCode.png --bootstrap-method QRCodePNG aws iam enable-mfa-device \ --user-name alice \ --serial-number arn:aws:iam::123456789012:mfa/alice-mfa \ --authentication-code1 123456 --authentication-code2 789012
An IAM policy Condition can also REQUIRE MFA presence before allowing a sensitive action at all (already shown in the trust-policy example earlier) — turning MFA from a login-time nicety into an enforced, per-action gate for the most sensitive operations.
IAM Policy Variables and ABAC — Attribute-Based Access Control#
A genuinely powerful, less commonly known technique worth understanding well: instead of writing a SEPARATE policy statement for every individual resource, a policy can reference the REQUESTING principal's own tags as a variable, letting ONE policy correctly scope access differently for every different user or role.
Diagram
# A SINGLE policy, attached broadly, that only allows access # to EC2 instances whose "Team" tag matches the CALLING # principal's OWN "Team" tag — using the aws:PrincipalTag # policy variable cat <<'ABAC' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances"], "Resource": "*", "Condition": { "StringEquals": { "aws:ResourceTag/Team": "${aws:PrincipalTag/Team}" } } }] } ABAC
Why this is worth stating explicitly as the modern, scalable alternative to hand-writing per-team policies, a genuinely strong interview answer: "with ABAC, adding a new team doesn't require writing a single new IAM policy at all — as long as the new team's IAM roles are tagged Team=X and their resources are tagged the same way, the existing, single ABAC policy automatically scopes correctly for them, entirely through the tagging discipline already established in Part 1, rather than through IAM policy proliferation." This directly reduces the exact "N teams × M resources" policy-maintenance burden that traditional role-based, resource-ARN-enumerated policies accumulate at scale — a genuinely important distinction between RBAC-style and ABAC-style IAM design worth being able to articulate precisely.
# Tag the IAM role itself with the matching attribute — # THIS is what aws:PrincipalTag actually reads from aws iam tag-role --role-name payments-team-role --tags Key=Team,Value=payments
Auditing IAM: Access Analyzer and Credential Reports#
# Generate an account-wide credential report — every IAM user, # when their password/access keys were last used or rotated aws iam generate-credential-report aws iam get-credential-report --query 'Content' --output text | base64 -d # IAM Access Analyzer flags resources shared with entities # OUTSIDE the account/organization — catching accidental # over-broad resource-based policies aws accessanalyzer list-findings --analyzer-arn <analyzer-arn>
Why an unused access key sitting untouched for 6+ months is a real, flaggable risk worth explaining: it's a standing credential that provides zero ongoing business value but still represents a full attack surface if leaked — the credential report is precisely the tool that surfaces exactly this class of finding for a routine access review, directly connecting to the "audit evidence" discussion in the DevSecOps series' compliance part.
Permission Boundaries — A Ceiling for a Single Identity#
Worth distinguishing precisely from SCPs (Part 1), since both use the word "boundary" or "ceiling" but operate at different scopes. A Permission Boundary is an advanced IAM feature that sets the maximum possible permissions for ONE specific IAM user or role — not an entire account or OU like an SCP.
Diagram
# A permission boundary limiting a role to ONLY S3 and CloudWatch # actions, no matter what its actual permission policy grants cat <<'BOUNDARY' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:*", "cloudwatch:*", "logs:*"], "Resource": "*" }] } BOUNDARY aws iam create-role \ --role-name AppDeployRole \ --assume-role-policy-document file://trust-policy.json \ --permissions-boundary arn:aws:iam::123456789012:policy/S3AndCloudWatchOnly
Why this is the practical tool for a genuinely common, real problem — "let a CI/CD pipeline create IAM roles for individual applications, without letting it accidentally (or maliciously) create a role with admin access" — worth explaining precisely: attach a Permission Boundary to every role the pipeline is allowed to create, capping what ANY role it creates can ever do, regardless of what permission policy someone later attaches to it. This is the concrete AWS mechanism behind a genuinely important delegation pattern: letting a less-trusted process create IAM identities without that process being able to escalate privilege beyond an explicit, pre-approved ceiling.
Service-Linked Roles#
A special category of IAM role, pre-defined and used by an AWS service itself to perform actions on your behalf — worth recognizing by name since they show up constantly in account IAM listings and are, deliberately, harder to delete than an ordinary role.
# Many AWS services create their service-linked role automatically # on first use; you can also create one explicitly aws iam create-service-linked-role --aws-service-name elasticloadbalancing.amazonaws.com # List all service-linked roles in the account aws iam list-roles --query 'Roles[?contains(Path, `/aws-service-role/`)].RoleName'
Why AWS deliberately restricts deleting a service-linked role while it's still in use, worth stating explicitly: it prevents accidentally breaking a service's ability to manage its own resources on your behalf — e.g. deleting the ELB service-linked role while load balancers still exist would leave AWS unable to manage their underlying network interfaces — a genuinely sensible guardrail, not an arbitrary restriction.
Break-Glass Access — Emergency Procedures#
A genuinely important, real operational pattern worth knowing by name: a pre-provisioned, tightly controlled, heavily audited IAM role or credential set reserved specifically for emergencies where normal access paths (federation via a corporate IdP, Part 2's earlier section) are themselves unavailable — e.g. the corporate IdP itself is down during a P1 incident.
Diagram
Why a break-glass account should trigger an IMMEDIATE, automatic alert on every single use, worth stating explicitly, directly connecting to the alerting-design discipline from the Observability series: using it should never be a routine, unnoticed event — a CloudTrail-based alarm firing to the security team the instant break-glass credentials are used means even a legitimate emergency use gets an automatic audit trail and follow-up review, and an illegitimate use is caught immediately.
# A CloudWatch alarm wired to a CloudTrail event pattern # specifically watching for break-glass role usage aws events put-rule \ --name break-glass-usage-alert \ --event-pattern '{"source":["aws.sts"],"detail":{"userIdentity":{"arn":[{"prefix":"arn:aws:sts::123456789012:assumed-role/BreakGlassRole"}]}}}'
A Deeper Policy Evaluation Example — Deny With a Condition#
Worth working through one more concrete, precise example combining several concepts from this part at once, since these combined-condition scenarios are genuinely common in real interview questions.
cat <<'CONDPOLICY' { "Version": "2012-10-17", "Statement": [{ "Sid": "DenyUnlessFromCorpVPN", "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "NotIpAddress": { "aws:SourceIp": ["203.0.113.0/24"] }, "BoolIfExists": { "aws:ViaAWSService": "false" } } }] } CONDPOLICY
Walking through exactly what this does, worth stating precisely: it denies EVERY action, for whoever it's attached to, UNLESS the request originates from the specific 203.0.113.0/24 IP range (e.g. a corporate VPN egress) — and the BoolIfExists condition specifically avoids accidentally blocking legitimate AWS-service-to-service calls (which don't have a normal source IP in the traditional sense) that are made "via" another AWS service on your behalf. This is a genuinely realistic pattern for a highly sensitive role (e.g. one able to modify production IAM policies itself) that should only ever be usable from a trusted network location, layering a network-location condition on top of everything else this part has covered.
Part 2 CLI Cheat Sheet#
| Area | Command | Purpose |
|---|---|---|
| Identity | aws sts get-caller-identity | Confirm current identity |
| Identity | aws sts assume-role | Assume an IAM role, get temporary credentials |
| Users | aws iam create-user / attach-user-policy | Create a user, attach a managed policy |
| Groups | aws iam create-group / add-user-to-group | Create a group, add a user to it |
| Roles | aws iam create-role | Create a role with a trust policy |
| Roles | aws iam create-service-linked-role | Create a service-linked role |
| Policies | aws iam create-policy | Create a customer-managed policy |
| Policies | aws iam simulate-principal-policy | Test what a principal can/can't do, without making the real call |
| MFA | aws iam enable-mfa-device | Enable MFA on a user |
| Auditing | aws iam generate-credential-report / get-credential-report | Generate and fetch account-wide credential usage report |
| Auditing | aws accessanalyzer list-findings | List external-access findings |
| Federation | aws sso login | Log in via IAM Identity Center (AWS SSO) |
IAM Best Practices — The Consolidated Checklist#
A dense, exam-and-interview-ready summary of every recommendation made across this part — worth keeping as a standing reference for real IAM design reviews.
- Lock the root user away with MFA immediately, and never use it for routine work — no policy can ever restrict it.
- Prefer IAM roles with temporary credentials over IAM users with long-lived access keys, for both humans (via federation) and workloads (via instance profiles/IRSA/OIDC).
- Scope
Resourceto specific ARNs, never leave"*"in a policy meant for production use. - Use security-group-style least-privilege thinking for IAM too — grant the narrowest permission set a role's actual job requires, and use Access Analyzer's policy generation to verify this against real usage.
- Require MFA in the trust policy of any genuinely sensitive role, on top of whatever MFA the human's normal login already requires.
- Federate human access through a corporate IdP via IAM Identity Center rather than managing IAM users per account — this is the single biggest offboarding-speed improvement available.
- Use Permission Boundaries when delegating role-creation ability to a less-trusted process (a CI/CD pipeline, a self-service portal) — this caps what any role IT creates can ever do.
- Consider ABAC (tag-based policies) once policy count starts scaling with team/resource count — it eliminates an entire class of policy-proliferation maintenance burden.
- Maintain a break-glass access path, independent of the normal federated login flow, with automatic, immediate alerting on every single use.
- Run credential reports and Access Analyzer regularly, not just once at initial setup — access needs drift over time, and unused credentials are a standing, silent risk.
A Full Worked Example: Designing IAM for a Three-Tier Application#
Bringing this entire part together into one concrete, complete design — genuinely worth walking through end to end, since "design the IAM for this application" is an extremely common real interview prompt.
Diagram
Walking through each identity and WHY it's scoped the way it is:
- The app server's instance profile role grants exactly two things: read access to one specific S3 prefix holding its own configuration, and read/write access to one specific DynamoDB table — never a blanket
s3:*ordynamodb:*, directly applying the least-privilege discipline from this part. - The CI/CD deploy role, assumed cross-account from a dedicated tooling account (Part 11 covers this pattern in depth), is bounded by a Permission Boundary limiting it to ONLY modifying this specific application's deployment resources — even if a mistake in the pipeline's own IAM policy granted broader access, the boundary prevents it from ever taking effect.
- Database access uses IAM database authentication (covered further in Part 9) rather than a static database password — meaning there's no long-lived database credential to rotate, leak, or manage at all, extending the "prefer temporary, automatically-managed credentials" theme from this entire part down to the database layer itself.
Why walking through a design this way — identity by identity, each with an explicit, stated reason for its exact scope — is worth practicing out loud, a genuinely strong interview habit: it demonstrates the difference between "I'll just attach AdministratorAccess to get this working" and actually reasoning through least privilege for each distinct actor in a system, which is precisely what separates a junior answer from a senior one on this topic.
Common Mistakes#
| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Using the root user for day-to-day work | No IAM policy or SCP can restrict root — the largest possible blast radius in the account | Lock root away with MFA immediately; use IAM roles/users for everything else |
| Creating long-lived IAM user access keys for applications/CI systems | A standing credential that can leak and doesn't expire on its own | Use IAM roles with temporary credentials (instance profiles, IRSA, OIDC federation for CI) instead |
Writing "Resource": "*" in a policy "to get it working," then forgetting to narrow it | Grants far more access than intended, violating least privilege | Scope Resource to specific ARNs from the start; treat "*" as a temporary debugging step only |
| Assuming an SCP "Allow" statement grants a permission | SCPs are ceilings, not grants — a common Part 1/Part 2 crossover mistake | Remember: SCP sets the max possible; IAM policy still has to separately grant the actual permission |
| Manually creating and tracking IAM users per employee at a growing organization | Doesn't scale, and access isn't revoked when someone leaves unless someone remembers to delete the IAM user | Federate through a corporate IdP via IAM Identity Center instead |
| Sharing one broad EC2 instance-profile role across many unrelated pods/services on Kubernetes | Every workload on the node gets the same broad permissions, violating least privilege | Use IRSA (or the equivalent for other clouds) to scope IAM permissions per Kubernetes ServiceAccount |
Worked Practice Problems#
Problem 1: A developer reports that their IAM user, which has an attached policy explicitly granting s3:* on all resources ("Resource": "*"), still cannot delete objects from a specific bucket named compliance-archive. What's the most likely explanation, and how would you confirm it?
Answer: Given the broad Allow already in place, the most likely explanation is an explicit Deny somewhere else in the evaluation chain — either a separate identity-based policy attached to the same user/group scoped specifically to that bucket, a resource-based bucket policy on compliance-archive itself denying deletes, or an SCP at the OU/account level blocking deletion on that resource. Since an explicit Deny always wins regardless of how broad any Allow is, this is a completely consistent, expected outcome, not a bug. To confirm it, use the IAM Policy Simulator or aws iam simulate-principal-policy against the specific s3:DeleteObject action and resource ARN — it will show exactly which statement is producing the Deny.
Problem 2: A security review finds an EC2 instance running application code that has a hard-coded AWS access key and secret in its environment variables, granting broad S3 and DynamoDB access. The team wants to eliminate this without changing how the application code calls AWS services. What's the fix, and why does it not require an application code change?
Answer: Replace the hard-coded access key with an IAM instance profile attached to the EC2 instance, containing a role scoped to only the specific S3/DynamoDB permissions actually needed. This requires no application code change because the AWS SDK's default credential resolution chain already checks the local Instance Metadata Service (IMDS) automatically, before falling back to explicitly-set environment variables — once the environment variables are removed, the SDK will transparently start using the instance profile's automatically-rotating temporary credentials instead, with zero code changes required.
Problem 3: An organization with 200 employees across 15 AWS accounts currently manages IAM users manually per account. When an employee leaves, IT reports it typically takes 2-3 days to fully revoke their AWS access across all accounts, because someone has to remember to find and delete their IAM user in every account individually. What architectural change would reduce this to near-instant, and why?
Answer: Adopt federation through a corporate identity provider using AWS IAM Identity Center, replacing individual per-account IAM users with role access granted based on the employee's group membership in the central IdP. With this architecture, there is exactly ONE place access needs to be revoked: disabling the employee's account in the corporate IdP (the same system HR/IT already uses for email, Slack, etc.) immediately invalidates their ability to federate into any AWS account, with no need to separately hunt down and delete IAM users scattered across 15 different account boundaries.
Problem 4: A platform team wants to let application teams self-service create their own IAM roles for their services via a CI/CD pipeline, without needing a platform engineer to manually review and create every single role. The concern raised is that a team could, intentionally or by mistake, create a role with far more permission than their service actually needs, including full administrative access. What IAM feature directly addresses this concern while still allowing the self-service workflow?
Answer: A Permission Boundary attached to the IAM role the CI/CD pipeline itself uses when creating new roles on behalf of application teams. By setting a Permission Boundary as a mandatory condition on any role-creation call the pipeline performs, every role it creates is capped at that boundary's maximum permissions, no matter what permission policy is later attached to it — an application team could still misconfigure their own service's specific permissions within that ceiling, but could never accidentally or intentionally create a role with genuinely unbounded access, since the boundary itself cannot be exceeded regardless of what policy is attached.
Problem 5: During a major incident, the team discovers their standard access path — federated login through the corporate identity provider — is completely unavailable because the IdP itself is experiencing an outage, and engineers cannot get into the AWS account to begin remediation. What should have been in place to prevent this from blocking incident response, and what operational safeguard should accompany it?
Answer: A pre-provisioned break-glass access mechanism — a small number of emergency IAM credentials, sealed in a secure vault (not dependent on the corporate IdP), reserved specifically for exactly this scenario where the normal federated access path is itself unavailable. The critical accompanying safeguard is automatic, immediate alerting on any use of the break-glass credentials (e.g. a CloudWatch alarm on the relevant CloudTrail event pattern) — this ensures that even a legitimate emergency use during an incident gets flagged for mandatory follow-up review, and any illegitimate use is caught the moment it happens, rather than break-glass access becoming an untracked, unaudited backdoor.
Summary and What's Next#
- IAM governs literally every AWS API call — authentication proves who you are, authorization determines what you're allowed to do, evaluated fresh on every single request.
- The root user is unrestrictable by any policy — lock it away with MFA and do essentially all real work through IAM users or, preferably, roles.
- IAM roles, not long-lived IAM user access keys, are the modern best practice — they provide temporary, automatically-expiring credentials via AWS STS, governed by a trust policy (who can assume it) and a permission policy (what it can do).
- Evaluation logic, precisely: SCP deny wins first, then any explicit Deny anywhere wins, then any explicit Allow grants access, and everything else is implicitly denied by default.
- Instance profiles (for EC2) and IRSA (for EKS service accounts) are the concrete mechanisms that let compute resources get temporary, narrowly-scoped IAM credentials without any hard-coded secret ever touching disk.
- Federation through a corporate IdP via IAM Identity Center centralizes human access management to one place, avoiding the scaling and offboarding problems of per-account IAM users.
- Access Analyzer and credential reports are the real, concrete tools for auditing and tightening permissions toward genuine least privilege over time.
Continue to Part 3 (03-compute-ec2-and-autoscaling.md) to see how IAM roles attach to the actual compute layer — EC2 instances, Auto Scaling Groups, and the operational realities of running virtual machines in AWS.