Domain 1 of 4 · Chapter 1 of 3

Secure Access to AWS Resources

The four-tier IAM evaluation: SCP → boundary → identity policy → resource policy

"Why was this API call denied?" is the most-tested troubleshooting question in this domain, and one rule answers most of them: an explicit Deny in any policy layer beats every Allow. Around that rule sits a fixed four-layer evaluation, and tracing a request through it is the difference between debugging access in five minutes and three hours. (New to reading an Effect/Action/Resource policy statement? The CLF-C02 access-management guide covers the syntax; here the focus is how the layers interact.)

The four layers split into two families. Granting layers (identity policies and resource policies) are where permissions come from. Restricting layers (SCPs and permission boundaries) are ceilings: they cap what the granting layers can deliver, and never grant anything themselves. Both ceilings work identically: the action must appear in the ceiling's Allow statements, or the request is implicitly denied. They differ in exactly one thing (who the ceiling covers). An SCP covers every principal in an account or OU; a permission boundary covers the one IAM user or role it is attached to.

Layer 1, Service Control Policy (SCP). The organization-wide ceiling. Attached at AWS Organizations root, OU, or account; covers every principal in member accounts, including each member account's root user (the management account is exempt; the Permission boundaries vs SCPs section returns to this). If an SCP denies the action, evaluation stops, no further layer can grant it.

Two statements about SCPs sound contradictory until one sentence reconciles them: absent means no restriction, present but silent means deny. If no SCP applies to the account (a standalone account outside Organizations, or only the default FullAWSAccess policy attached), there is no ceiling and evaluation simply moves on; but if an SCP does apply and its Allow statements don't cover the action, the action is implicitly denied. The official IAM policy evaluation reference[13] states both halves in one sentence: "If there is no SCP, or if the SCP allows the requested action, the enforcement code evaluation continues."

Layer 2, Permission boundary. The per-principal ceiling. Attached to an individual IAM user or role; the boundary itself grants nothing. Its gate works exactly like the SCP gate: no boundary attached means no ceiling; a boundary that doesn't list the action means deny, even if the identity policy says yes.

Layer 3, Identity policy. The first granting layer. Attached to the user / role / group performing the action. Must contain an explicit Allow for the API action and resource. Identity policies are additive; multiple attached policies are unioned.

Layer 4, Resource policy. The second granting layer, attached to the target resource (S3 bucket, KMS key, SQS queue, etc.). This layer is where the same-account vs cross-account rule lives, stated once here, referenced everywhere else. Same account: an Allow in either the identity policy or the resource policy is sufficient; AWS takes the union of the two. The exceptions are services like KMS, whose key policies must allow the principal explicitly. Cross account: you need both sides; the resource policy must allow the foreign principal (or its account), and that principal's identity policy must allow the action.

Decision algorithm (simplified):

  1. Any explicit Deny in any layer? → DENY.
  2. An SCP applies but doesn't Allow the action? → DENY (no applicable SCP = no ceiling; this gate is skipped; see Layer 1).
  3. A boundary is attached but doesn't Allow the action? → DENY (no boundary = gate skipped).
  4. Apply the same-/cross-account rule from Layer 4: same account, either grant suffices; cross account, both required.
  5. Nothing allowed it? → DENY (default).

Common failure modes:

  • 'Admin user can't access S3 bucket': resource policy on bucket explicitly denies their principal. Explicit Deny beats anything.
  • 'New developer with PowerUser permissions can't create role': SCP at OU level denies iam:CreateRole for that OU.
  • 'Lambda execution role has admin but still can't decrypt KMS key': KMS key policy doesn't grant the role; KMS treats key policy as the primary access mechanism.

Exam shortcut: when a question lists multiple policies and asks what happens, evaluate from outer (SCP) to inner (resource) and stop at the first hard Deny.

API request Explicit Deny in any layer wins one explicit Deny ends it: DENY, overriding every Allow RESTRICTING family: ceilings cap only, never grant Layer 1 · SCP Layer 2 · Permission boundary absent = gate skipped · present but silent = deny GRANTING family: one must explicitly allow Layer 3 · Identity policy Layer 4 · Resource policy same account: either allows · cross account: both must ALLOW no grant anywhere = DENY (default)
Four IAM layers in two families: restricting ceilings (SCP, boundary) only cap; granting policies (identity, resource) must allow; any explicit Deny overrides all.

Trust policy authoring: cross-account, OIDC, SAML scenarios with ExternalId

A trust policy is the resource-based policy on an IAM role that controls who can assume it. Getting it right is the most common cross-account exam topic.

Anatomy:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::222222222222:root" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "unique-tenant-id-xyz" },
      "Bool": { "aws:MultiFactorAuthPresent": "true" }
    }
  }]
}

Read that Principal line carefully, because almost everyone misreads it the first time: arn:aws:iam::222222222222:root does not mean "only account 222222222222's root user". The :root suffix is the standard syntax for account-level trust: it delegates trust to the entire account, so every IAM user and role in account 222222222222 whose own policies permit sts:AssumeRole can assume this role. To trust a single principal, name it explicitly: arn:aws:iam::222222222222:role/Build.

Principal forms:

  • "AWS": "arn:aws:iam::222222222222:root" (account-level trust, the whole account, as explained above)
  • "AWS": "arn:aws:iam::222222222222:role/Build" (only this specific role)
  • "Federated": "cognito-identity.amazonaws.com" (Cognito Identity Pool)
  • "Federated": "arn:aws:iam::111...:oidc-provider/token.actions.githubusercontent.com" (GitHub Actions OIDC)
  • "Federated": "arn:aws:iam::111...:saml-provider/Okta" (SAML)
  • "Service": "lambda.amazonaws.com" (AWS service, for service roles)

Critical conditions:

  • sts:ExternalId[12]: required defence against the confused deputy problem. When a third-party SaaS assumes your role, only pinning the principal ARN is unsafe: another customer of the same SaaS could be tricked into assuming your role. The SaaS supplies a per-customer ExternalId; your trust policy validates it.
  • aws:MultiFactorAuthPresent: require MFA on the assuming session. For human-assumed roles (break-glass, admin elevation), this is the right default.
  • aws:SourceIp / aws:SourceVpc: restrict where the assume can originate (e.g. only from your VPN's IP range).
  • aws:PrincipalOrgID: require the caller's account to be in your Organization[15]. Cleaner than listing individual account ARNs.

OIDC for GitHub Actions, full pattern (uses the sts:AssumeRoleWithWebIdentity[6] API):

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
    }
  }
}

The sub claim restricts to a specific repo + branch. Wildcards (repo:my-org/*) let any repo in the org assume the role: sometimes desired, often a foot-gun.

Exam traps:

  • Principal: "*" in a trust policy = anyone in any AWS account can assume your role. Almost always wrong unless paired with restrictive conditions (and even then, suspect).
  • Forgetting sts:ExternalId for third-party integrations.
  • Long MaxSessionDuration (12h) when human elevation should be 1h or less.
Caller in account A wants sts:AssumeRole on role in B Trust policy on target role (B) names the caller's principal or account? no → DENY Condition block satisfied? ExternalId (confused deputy), MFA if set no → DENY Caller's identity policy in A allows sts:AssumeRole on that role? no → DENY Temporary session issued short-lived credentials via STS Cross-account: BOTH sides required trust policy (B) AND caller identity policy (A) must allow Same account: an Allow on either side is enough Principal "*" trusts every AWS account — almost always wrong
Cross-account sts:AssumeRole: the target role's trust policy plus its ExternalId/MFA conditions plus the caller's own identity policy must all allow.

Permission boundaries vs SCPs: matching the tool to the unit of governance

The four-tier evaluation section introduced the shared model: both permission boundaries[9] and Service Control Policies[10] are ceilings. They cap what the granting layers can deliver, and never grant anything themselves. What separates them is the unit of governance: who the ceiling covers, and who sets it.

Aspect Permission boundary Service Control Policy
Attaches to Individual IAM user or IAM role OU, account, or Organization root
Affects The single principal it's attached to Every principal in every member account in scope (including each member account's root user)
Use case Delegating role creation safely, "developer can create roles, but not above this ceiling" Organization-wide guardrail, "no IAM user with console access anywhere"
Set by Account admin Organization management account
Bypassable by root user? N/A (boundary scope is per-principal) Member-account root: NO, the ceiling applies. Management account: exempt entirely
Default if absent No ceiling, the boundary gate is skipped No ceiling, the default FullAWSAccess policy is attached (no restriction)

Pattern 1, Boundary for developer delegation:

Team X wants developers to create their own IAM roles for their Lambda functions, without granting full iam:*. Solution:

  1. Define a boundary policy DevBoundary that allows only the actions developer-created roles should be able to use (e.g. S3 read-only on team buckets, DynamoDB CRUD on team tables, CloudWatch Logs).
  2. Grant developers a policy that includes iam:CreateRole + iam:PutRolePolicy with the condition iam:PermissionsBoundary = arn:.../DevBoundary. They must attach the boundary when creating roles.
  3. Now the developer can iam:CreateRole a Lambda role, but every role they create is capped by DevBoundary. They can't accidentally grant themselves admin.

Pattern 2, SCP for organization-wide guardrail:

Company policy: no IAM user with console access can exist anywhere in the organization. Solution:

  1. SCP attached at organization root:
{ "Effect": "Deny", "Action": "iam:CreateLoginProfile", "Resource": "*" }
  1. Now no IAM user in any member account can get a console password. Note the deliberate gap: SCPs never affect the management account[10], neither its users nor its root, which is exactly why AWS guidance says to run no workloads there.

Pattern 3, Combining both:

SCP at OU 'Production' denies ec2:TerminateInstances for any principal except the role EmergencyAdmin. Inside one production account, a developer needs to manage their team's instances. Their role has a permission boundary that allows ec2:Start*, ec2:Stop*, ec2:Reboot* but NOT Terminate*. Boundary reinforces the SCP, and even if their identity policy granted ec2:*, both layers deny Terminate.

Common exam distractor: the question presents a scenario where central security wants to restrict an entire OU, and the option offering 'Permission boundary on every role in the OU' is plausible but wrong. Boundaries don't propagate; SCP is the right tool. The reverse is also tested: 'delegate role creation to one team' is a permission-boundary scenario, not SCP.

SCP — organization-wide ceiling OU: Production member acct A member acct B covers EVERY principal inside — including each member account's root user management account — exempt Permission boundary — per-principal ceiling one IAM role covers ONLY the user or role it is attached to same ceiling mechanics as the SCP gate: absent = skipped, silent = deny
The unit of governance: one SCP ceils every member account in scope (management account exempt); one permission boundary ceils a single principal.

IAM Identity Center deep dive: permission sets, account assignments, IdP wiring

IAM Identity Center (formerly AWS SSO) is the modern primitive for workforce access, your employees reaching AWS accounts. (Machine workloads are the next section's topic.) It replaces per-account IAM federation with centralised user + group management.

Core concepts:

  • Identity source. Where the users live. Three options:
    • Identity Center directory (built-in; OK for small teams).
    • Active Directory (on-prem AD via AWS Managed Microsoft AD or AD Connector).
    • External IdP (Okta, Entra ID, Google Workspace, OneLogin, etc.) via SAML 2.0 + SCIM for user/group sync.
  • Permission set. A blueprint of IAM policies that defines a role identity-center will create in target accounts when assigned. Think of it as 'reusable role template'.
  • Account assignment. Binding: (Group X) + (Account Y) + (Permission Set Z). When a user logs in, the user picks one of their assigned account + permission-set combinations; Identity Center provisions a temporary session via AssumeRole into the target.

Architecture: the figure above traces the flow. The external IdP authenticates the user via SAML (users and groups synced via SCIM), an account assignment binds group × account × permission set, and the portal click ends in an AssumeRole into the target account for temporary credentials (1–12 hours).

Where Identity Center lives:

  • One region per organization; pick carefully; can't be moved post-setup.
  • Must be enabled from the management account of the AWS Organization.
  • Requires AWS Organizations (cannot run standalone).

Permission set scoping:

  • Up to 50 managed policies + 1 customer-managed policy + inline policy per permission set.
  • Session duration: 1 to 12 hours (controlled at permission-set level).
  • Permission sets can include a permissions boundary; adds another ceiling on top of the policies.

Common patterns:

  • Just-in-time admin elevation: regular users have read-only permission set; on-call engineer gets a 'BreakGlassAdmin' permission set assigned for a 1-hour shift. Trail logs every assumption.
  • Multi-account access: one user account in Identity Center; assignments to 30 AWS accounts with role-appropriate permission sets in each. No per-account IAM users to manage.
  • Audit access: read-only permission set assigned to the audit team across all accounts; no production write capability ever.

Anti-patterns the exam will probe:

  • Creating IAM users in each member account 'for the developer to log in'; wrong; permission set + account assignment is the answer.
  • Using Identity Center for application end-user authentication; wrong; that's Cognito User Pools. Identity Center is for workforce (employees) accessing AWS.
  • Trying to enable Identity Center from a non-management account; fails by design; must be management account. See What is IAM Identity Center?[7] for the canonical setup walkthrough.
External IdP (Okta, Entra ID) SAML login SCIM user/group sync IAM Identity Center Account assignment group × account × permission set user clicks account in portal AssumeRole into target account temporary credentials session duration 1–12 hours
Workforce sign-in flow through IAM Identity Center: SAML + SCIM in, AssumeRole with temporary credentials out.

Workload identity patterns: IRSA (EKS), OIDC for GitHub Actions, EC2 IMDSv2

Workload identity = how a non-human process (container, Lambda, on-prem CI runner) gets temporary AWS credentials without storing a long-lived secret. The pattern across all five mechanisms below is the same: AWS trusts the issuer (IMDS, EKS OIDC, GitHub OIDC), validates the workload's identity against that trust, and hands out short-lived credentials. No long-lived secret ever lives in the workload.

1. EC2 — Instance Metadata Service (IMDS).

The EC2 instance has an instance profile (an IAM role[1]); the AWS SDK on the instance fetches temp credentials from 169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>. Credentials rotate automatically.

IMDSv1 vulnerability: Server-Side Request Forgery (SSRF) attacks can trick a vulnerable app into requesting credentials from the metadata service and leaking them.

Fix — enforce IMDSv2[2]: requires a session token (PUT request) before any credentials fetch. SSRF can't follow the multi-step protocol. Enforce via:

  • New instances: launch template / RunInstances with HttpTokens=required.
  • Existing instances: aws ec2 modify-instance-metadata-options --http-tokens required.
  • Org-wide: SCP denying instance launches without HttpTokens=required.

2. ECS — Task Role[4].

Every task in a task definition can have an IAM role. The ECS agent fetches temp credentials and exposes them at a task-local endpoint (169.254.170.2/v2/credentials/<id>). The SDK auto-discovers via env var AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.

Distinct from execution role:

  • Execution role: ECS agent uses this to pull container images from ECR, write logs to CloudWatch. Operational.
  • Task role: the application inside the container uses this to call AWS APIs. App-specific.

Common mistake: putting app-needed permissions on execution role. Wrong layer.

3. EKS — IAM Roles for Service Accounts (IRSA)[5].

Kubernetes pods can have an IAM role mapped via the pod's service account. The pod gets a projected token; the AWS SDK exchanges it for temp credentials via sts:AssumeRoleWithWebIdentity[6].

Setup:

  1. Enable OIDC provider on the EKS cluster (eksctl utils associate-iam-oidc-provider).
  2. Create IAM role with trust policy permitting the OIDC provider + a specific service account namespace.
  3. Annotate the Kubernetes service account: eks.amazonaws.com/role-arn: arn:aws:iam::...:role/MyRole.
  4. Pods using that SA automatically get credentials.

Much better than the alternative: kube2iam or instance-profile inheritance (which means every pod on the node has the same permissions — least-privilege violation).

4. GitHub Actions — OIDC web-identity.

GitHub Actions runners can request a JWT from GitHub's OIDC provider. AWS trusts that provider via an IAM OIDC identity provider in your account, then runners call sts:AssumeRoleWithWebIdentity directly.

Setup:

  1. In your AWS account, create an OIDC identity provider for https://token.actions.githubusercontent.com (thumbprint and audience pre-known).
  2. Create an IAM role with a trust policy pinning the OIDC issuer + audience + sub (repo + branch).
  3. In the Actions workflow, use aws-actions/configure-aws-credentials@v4 with role-to-assume — no AWS access keys in repo secrets.

Result: short-lived credentials (default 1h) bound to a specific repo + branch. Compromised workflow = limited blast radius.

5. Lambda — execution role[3].

Trivially the simplest: set role at function creation. Credentials available via env vars and the SDK auto-discovers. Rotated per invocation by the Lambda runtime.

Trusted issuer (the source AWS trusts) EC2 IMDSv2 OIDC provider (EKS, GitHub) Step 1 · AWS trusts the issuer IAM role names the issuer in its trust Step 2 · Validate the workload identity checked against that trust Step 3 · Short-lived credentials rotated automatically; no stored secret Same shape for all five: EC2, ECS, EKS, GitHub Actions, Lambda No long-lived secret ever lives in the workload
The single workload-identity pattern behind all five: AWS trusts the issuer (IMDSv2 or OIDC), validates the workload, and issues short-lived credentials.

Common cross-account antipatterns and the policies that fix them

Antipattern 1: Sharing an IAM user's access keys across accounts.

Wrong: 'Developer needs to deploy to dev and prod, so we put their keys in both.' This violates the IAM security best practices[17] recommendation against long-lived credentials.

Right: Create roles DeployDev in dev account, DeployProd in prod account. Trust policies permit the developer's IAM principal in their home account. They aws sts assume-role to switch context. Audit trails show which role was used in which account.

Antipattern 2: Principal: * in bucket policies for 'partner' access.

Wrong: 'Partner needs to upload to our S3 bucket, so we made it public for writes.'

Right: Resource policy permits the partner's specific account (or role):

{ "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::PARTNER_ACCOUNT:root" },
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::our-bucket/uploads/*",
  "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } }
}

The bucket-owner-full-control condition ensures uploaded objects are owned by us, not the partner; otherwise we can't read what they upload.

Antipattern 3: Forgetting sts:ExternalId for third-party SaaS roles.

Wrong: SaaS vendor (monitoring, backup, security) gives you a CloudFormation template that creates a role with trust policy Principal: arn:aws:iam::VENDOR:root. Anyone at the vendor with your role ARN can assume it.

Right: require the vendor's per-tenant ExternalId in the trust policy's Condition, the confused-deputy defence explained in Trust policy authoring above.

Antipattern 4: Listing individual partner account ARNs in 50 places.

Wrong: 'We have 30 partner accounts; we maintain a list in every bucket policy and trust policy.'

Right: If partners are all in the same AWS Organization, use aws:PrincipalOrgID:

"Condition": {
  "StringEquals": { "aws:PrincipalOrgID": "o-abc123def4" }
}

New partner joins the org → automatically covered.

Antipattern 5: Granting full IAM admin to developers 'for convenience'.

Wrong: 'Developers need to make IAM roles for their Lambdas, so they have iam:*.'

Right: the developer-delegation boundary pattern from Permission boundaries vs SCPs (Pattern 1) above: grant iam:CreateRole / iam:PutRolePolicy only with a condition requiring iam:PermissionsBoundary = DevBoundary on every role they create. They can create roles, but every one is capped.

Antipattern 6: 'Centralised security account' that uses a hand-rolled cross-account scheme.

Wrong: Custom Lambda + custom roles + custom rotation script.

Right: AWS Organizations auto-creates OrganizationAccountAccessRole in every member account at the time they join the org. The management account (and any account given the right SCP scope) can assume it directly. Use this; don't build your own.

Identity primitives: when each is the right answer

PrimitiveCredential lifetimeHow it's obtainedBest forCommon trap
IAM userPermanent until rotated/deletedAccess key + secret created in console / CLILast-resort exception only (legacy SaaS, break-glass)Rotation discipline; orphaned keys; no MFA on programmatic users
IAM role (assumed)Temporary; default 1h, max 12h via MaxSessionDuration`sts:AssumeRole` + trust policy permits callerCross-account access, workload identity, just-in-time elevationForgetting to set MaxSessionDuration explicitly; trust policy too permissive
Instance profile (EC2)Temporary; rotated by IMDSAttached at instance launch or via Modify-Instance-AttributeEC2 instances calling AWS APIsIMDSv1 is SSRF-vulnerable: enforce IMDSv2 (`HttpTokens=required`)
Lambda execution roleTemporary; rotated per invocationSet at function creation; one role per functionLambda functions calling AWS APIsShared mega-role across many functions defeats least-privilege
ECS task roleTemporary; rotated per taskSet in task definition (distinct from execution role)Containers in tasks calling AWS APIsConfusing task role (app permissions) with execution role (image pull + logs)
Federated user (IAM Identity Center)Temporary; session duration capped by both IdP and roleIdP authentication → Identity Center → AssumeRoleWorkforce SSO at any scaleForgetting Identity Center is region-bound; pick the home region carefully
Federated user (OIDC)Temporary; per-token`sts:AssumeRoleWithWebIdentity` + IdP issuer + audienceGitHub Actions, IRSA on EKS, on-prem CITrust policy must pin `aud` and `sub`: wildcards = trust any caller
Cognito identity (end-user)Temporary; via Identity PoolUser Pool token exchanged at Identity PoolWeb/mobile end-users with direct AWS access (uncommon)Almost always wrong on SAA. Prefer API Gateway + Lambda + IAM role

Decision tree

Workload or workforce?WorkloadWorkforceRuns inside AWS compute?YesNo< 5 AWS-only users?YesNoInstance profile/ Lambda role/ ECS task roleSupports OIDC?YesNoIAM users+ MFA + consoleaccess onlyIAM IdentityCenter + IdP(SAML / OIDC)OIDC + AssumeRoleWithWebIdentity(GitHub, EKS, etc.)IAM user keys(last resort —rotate, audit)Always: MFA on root + console; SCPs at org root; Access Analyzer enabled

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

Give workloads an IAM role, never embedded static keys

An IAM role attached to compute hands out short-lived credentials that AWS rotates automatically, so there is nothing long-lived to leak: attach one to anything that calls AWS. Each service has its own role flavor: EC2 gets an instance profile, Lambda an execution role, ECS a task role, EKS uses IRSA, and GitHub Actions federates via OIDC AssumeRoleWithWebIdentity. Static access keys are the fallback only when something genuinely can't assume a role, like legacy SaaS that can't do OIDC.

Trap Baking long-lived access keys into an instance or your code: a permanent secret leaks once and stays compromised forever, whereas role credentials expire and rotate on their own.

3 questions test this
5+ humans → federate through IAM Identity Center, not IAM users

When workforce access reaches real scale, keep the identities in your existing IdP (AD, Okta, Entra, or Google) and bind them through Identity Center permission sets, assigned as (group × account × permission set) so access stays centralized and group-managed instead of being copied into every account. AWS renamed AWS SSO to IAM Identity Center[7] in 2022, so treat both names as one service. Stand-alone IAM users and self-managed federation fragment identity across accounts instead of driving it from one source.

Trap Reaching for local IAM users or Cognito to handle staff access: local IAM users only make sense for a 2-3-person startup, and Cognito User Pools are for your end-customers rather than staff, so neither will scale or centralize.

2 questions test this
A permission boundary caps maximum permissions, it never grants any

A permission boundary sets a ceiling on what an IAM principal can do, effective permissions are the intersection identity Allow ∩ boundary Allow ∩ no Deny ∩ no SCP Deny, so it can only ever subtract, never add. That makes it the right tool for delegated administration: developers create their own roles while the boundary guarantees those roles can never exceed it, stopping iam:* privilege escalation without you reviewing every role by hand.

Trap Expecting a permission boundary to grant access: it only ever intersects, so an action missing from the identity policy stays denied no matter what the boundary allows.

5 questions test this
Cross-account access = role with a trust policy + sts:AssumeRole

Cross-account access works by account B creating a role whose trust policy names principals in account A; A then calls sts:AssumeRole and receives temporary credentials, so access flows through short-lived sessions rather than shared secrets and every assume can be audited and conditioned. Harden the trust policy for the situation: add sts:ExternalId when the caller is a third-party SaaS to defeat the confused deputy, and require aws:MultiFactorAuthPresent when a human assumes the role.

Trap Sharing access keys across accounts just to skip setting up a role: that hands out permanent credentials and loses the per-assume conditions like ExternalId and MFA that a trust policy gives you.

8 questions test this
Third-party assuming your role → pin sts:ExternalId, not just their ARN

When a SaaS vendor assumes a role in your account, pin sts:ExternalId[12] in the trust policy's Condition: the vendor issues a unique ID for your account and your trust policy checks it on every AssumeRole, rejecting any request that doesn't carry your specific ID. This closes the confused-deputy gap. The vendor's own role is the principal, but the ExternalId proves the request was meant for your account and not another customer's.

Trap Pinning only the vendor's principal ARN: it feels like enough but isn't, because another customer of that same vendor could be tricked into supplying your role ARN, the classic confused-deputy attack.

4 questions test this
SCPs only restrict member accounts; they never grant access

Service Control Policies[10] attach at an AWS Organizations root or OU and set the outer guardrail on what member accounts can do, capping every principal in those accounts. But an SCP only restricts, never permits, so it shapes the ceiling rather than opening a door. Granting actual access is still the job of identity and resource policies inside the accounts; the SCP just bounds what those policies are allowed to reach.

Trap Reaching for an SCP to let account B read a bucket in account A: that's the wrong layer, because access comes from a bucket policy in A plus a role in A that a principal in B assumes.

5 questions test this
Service needs to act on your behalf → it's a service-linked role

A service-linked role[11] is one AWS auto-creates, predefines, and manages when you enable a feature like Auto Scaling, Organizations, ECS, or Lambda@Edge, so the service has exactly the permissions it needs to act for you with no policy authoring from you. Because AWS owns the role's permissions, you can't reshape it the way you'd scope a normal role. The answer to "how does Service X act on my behalf" is usually that the SLR already exists.

Trap Hand-authoring a custom role for a feature that uses a service-linked role. AWS owns the SLR's permissions, so you can't reshape it the way you'd scope an ordinary role.

Block IMDS SSRF by enforcing IMDSv2 (HttpTokens=required)

IMDSv2[2] requires a caller to first fetch a session token with a PUT before any metadata GET, breaking the SSRF chain where an app flaw tricks the server into reading role credentials from 169.254.169.254. A forged GET can't complete the PUT handshake. Enforce it on the launch template with HttpTokens=required, or org-wide with an SCP that denies any launch not requiring it, so no instance can quietly fall back to token-less IMDSv1.

Trap Leaving IMDSv1 on just because the instance "is behind a load balancer": IMDSv1 needs no token, so any server-side request forgery in your app can reach the metadata endpoint and exfiltrate the role credentials.

An explicit Deny anywhere wins over every Allow

An explicit Deny in any policy layer short-circuits the whole evaluation[13], so no Allow anywhere can override it. IAM evaluates SCP, permission boundary, identity policy, and resource policy together, and a single Deny among them ends the decision. A deny is therefore the reliable way to carve out a hard exception, and troubleshooting "why is this blocked" means hunting for the explicit Deny rather than piling on more Allows.

Trap Assuming an admin with *:* can do anything. When "Admin can't access the bucket" the cause is almost always an explicit Deny on their principal in the resource policy.

1 question tests this
Role sessions are 1h by default, up to 12h via MaxSessionDuration

A role session defaults to 1 hour and extends through the role's MaxSessionDuration[14], settable between 1 and 12 hours, while IAM Identity Center permission sets carry their own 1–12h duration. Short sessions are the security default because the credentials expire on their own, so you raise the cap only as far as a genuinely long-running task needs.

Trap Expecting a federated session to run the full MaxSessionDuration: it's capped by both the IdP token lifetime and the role's MaxSessionDuration, and the shorter of the two wins.

Central admin into Org accounts → OrganizationAccountAccessRole

The OrganizationAccountAccessRole is created automatically with full admin in any account that joins AWS Organizations[15] (whether created in or invited into the org) and it trusts the management account, so the cross-account admin path is pre-wired and a central security account simply assumes it. That removes the per-account bootstrap of hand-building a trust role each time a new account joins.

Trap Hand-building a cross-account trust role in every new Org account: OrganizationAccountAccessRole is created automatically and already trusts the management account.

Find accidental public/cross-account grants with IAM Access Analyzer

IAM Access Analyzer[16] reasons over resource policies on S3 buckets, IAM roles, KMS keys, Lambda functions, SQS queues, and Secrets Manager and flags any grant to a principal outside your account or organization, catching the "this bucket policy accidentally allows the internet" problem before an attacker does. It's free to enable per region, the systematic alternative to manual policy review that can't keep up as policies multiply.

Trap Reaching for GuardDuty or Macie to catch an over-permissive resource policy: those watch threats and data, whereas IAM Access Analyzer reasons over the policies themselves for external access.

Grant a resource to your whole org with aws:PrincipalOrgID

The aws:PrincipalOrgID global condition key, placed in a resource policy such as a bucket policy, admits only principals whose accounts belong to the named AWS Organization, so you express "anyone in my org" once instead of enumerating account IDs. Because it matches on org membership rather than a fixed list, any account you add later is covered automatically with no policy edit.

Trap Listing every account ID in the Principal element instead: that doesn't scale and silently leaves out new accounts, whereas aws:PrincipalOrgID grows with the org.

4 questions test this
Cross-account S3 needs BOTH a bucket policy and an IAM policy

Cross-account S3 is authorized only by the intersection of both sides when the requesting identity and the bucket sit in different accounts: a bucket policy in the resource account granting the action to the cross-account principal, plus an identity-based policy in the requesting account allowing that principal to reach the bucket ARN. Either side alone leaves a gap, because each account independently has to consent to the access.

Trap Setting only the bucket policy (or only the IAM policy) for cross-account S3: either side alone leaves a gap, because both accounts must independently allow the access.

8 questions test this
Require recent MFA on a role with aws:MultiFactorAuthAge

The aws:MultiFactorAuthAge condition on a trust policy gates role assumption on recently re-authenticated MFA: set a NumericLessThan in seconds, where 3600 requires the MFA to be under an hour old. This puts sensitive actions behind a fresh step-up, because the condition measures how long ago the MFA happened rather than merely that it ever happened.

Trap Using aws:MultiFactorAuthPresent instead: it only confirms MFA happened at some point in the session, not how recently, so a session authenticated hours ago still passes.

Also tested in

References

  1. Official AWS guide to IAM roles
  2. Configure instance metadata options for existing instances
  3. Lambda execution role
  4. Task IAM role (Amazon ECS)
  5. IAM roles for service accounts (EKS / IRSA)
  6. AssumeRoleWithWebIdentity: AWS STS API reference
  7. What is IAM Identity Center?
  8. Amazon Cognito user pools
  9. IAM permissions boundaries for IAM entities
  10. Service control policies (SCPs)
  11. Using service-linked roles
  12. How to use trust policies with IAM roles (confused-deputy + ExternalId)
  13. IAM JSON policy evaluation logic
  14. Modify the maximum CLI/API session duration for a role
  15. What is AWS Organizations
  16. What is IAM Access Analyzer
  17. Security best practices in IAM