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):
- Any explicit
Denyin any layer? → DENY. - An SCP applies but doesn't
Allowthe action? → DENY (no applicable SCP = no ceiling; this gate is skipped; see Layer 1). - A boundary is attached but doesn't
Allowthe action? → DENY (no boundary = gate skipped). - Apply the same-/cross-account rule from Layer 4: same account, either grant suffices; cross account, both required.
- 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:CreateRolefor 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.
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:ExternalIdfor third-party integrations. - Long
MaxSessionDuration(12h) when human elevation should be 1h or less.
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:
- Define a boundary policy
DevBoundarythat 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). - Grant developers a policy that includes
iam:CreateRole+iam:PutRolePolicywith the conditioniam:PermissionsBoundary = arn:.../DevBoundary. They must attach the boundary when creating roles. - Now the developer can
iam:CreateRolea Lambda role, but every role they create is capped byDevBoundary. 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:
- SCP attached at organization root:
{ "Effect": "Deny", "Action": "iam:CreateLoginProfile", "Resource": "*" }
- 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.
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.
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.
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:
- Enable OIDC provider on the EKS cluster (
eksctl utils associate-iam-oidc-provider). - Create IAM role with trust policy permitting the OIDC provider + a specific service account namespace.
- Annotate the Kubernetes service account:
eks.amazonaws.com/role-arn: arn:aws:iam::...:role/MyRole. - 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:
- In your AWS account, create an OIDC identity provider for
https://token.actions.githubusercontent.com(thumbprint and audience pre-known). - Create an IAM role with a trust policy pinning the OIDC issuer + audience +
sub(repo + branch). - In the Actions workflow, use
aws-actions/configure-aws-credentials@v4withrole-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.
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
| Primitive | Credential lifetime | How it's obtained | Best for | Common trap |
|---|---|---|---|---|
| IAM user | Permanent until rotated/deleted | Access key + secret created in console / CLI | Last-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 caller | Cross-account access, workload identity, just-in-time elevation | Forgetting to set MaxSessionDuration explicitly; trust policy too permissive |
| Instance profile (EC2) | Temporary; rotated by IMDS | Attached at instance launch or via Modify-Instance-Attribute | EC2 instances calling AWS APIs | IMDSv1 is SSRF-vulnerable: enforce IMDSv2 (`HttpTokens=required`) |
| Lambda execution role | Temporary; rotated per invocation | Set at function creation; one role per function | Lambda functions calling AWS APIs | Shared mega-role across many functions defeats least-privilege |
| ECS task role | Temporary; rotated per task | Set in task definition (distinct from execution role) | Containers in tasks calling AWS APIs | Confusing task role (app permissions) with execution role (image pull + logs) |
| Federated user (IAM Identity Center) | Temporary; session duration capped by both IdP and role | IdP authentication → Identity Center → AssumeRole | Workforce SSO at any scale | Forgetting Identity Center is region-bound; pick the home region carefully |
| Federated user (OIDC) | Temporary; per-token | `sts:AssumeRoleWithWebIdentity` + IdP issuer + audience | GitHub Actions, IRSA on EKS, on-prem CI | Trust policy must pin `aud` and `sub`: wildcards = trust any caller |
| Cognito identity (end-user) | Temporary; via Identity Pool | User Pool token exchanged at Identity Pool | Web/mobile end-users with direct AWS access (uncommon) | Almost always wrong on SAA. Prefer API Gateway + Lambda + IAM role |
Decision tree
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
- A solutions architect is designing an application that runs on Amazon EC2 instances and needs to access objects in an Amazon S3 bucket…
- Within a single AWS account, a company's operators normally have read-only permissions. A few times per week they must perform a sensitive…
- A solutions architect is deploying an application on an Amazon EC2 instance. The application needs to read and write objects in an Amazon…
- 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.
- 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, stoppingiam:*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
- A company wants to allow its development team to create IAM roles for their applications while preventing privilege escalation. The…
- A company wants to allow developers to create IAM roles for their applications while ensuring the roles cannot have more permissions than…
- A company wants to allow its application developers to create and manage IAM roles for their workloads. However, the security team must…
- A company is implementing a delegated administration model where different teams can create IAM roles for their applications. The security…
- A company has an IAM user with an identity-based policy that allows full access to Amazon DynamoDB. The administrator has attached a…
- 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:AssumeRoleand 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: addsts:ExternalIdwhen the caller is a third-party SaaS to defeat the confused deputy, and requireaws:MultiFactorAuthPresentwhen 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
- A security engineer needs to enable an application running in AWS account A to access an Amazon S3 bucket in AWS account B. The application…
- A company has a production AWS account and a security audit account. The security team in the audit account needs to assume a role in the…
- A company has two AWS accounts: Account A (111111111111) for production workloads and Account B (222222222222) for its security team. The…
- A third-party SaaS provider needs to access resources in a company's AWS account to perform automated cost analysis. The company's security…
- A company in AWS account A needs to allow an application running in AWS account B to write objects to an Amazon S3 bucket in account A. The…
- A company wants to allow developers in a development AWS account to assume roles in a production AWS account to access Amazon S3 buckets.…
- A company manages multiple AWS accounts using AWS Organizations. Administrators in a central security account need to assume roles in…
- A company has a development AWS account and a production AWS account in the same AWS Organizations organization. Engineers who have IAM…
- 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'sCondition: 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
- A company grants a third-party vendor access to its AWS account by creating an IAM role that the vendor can assume. The vendor provides the…
- A company is onboarding a third-party SaaS vendor to monitor its AWS infrastructure. The vendor requires cross-account access to assume an…
- A third-party SaaS provider needs to access resources in a company's AWS account to perform automated cost analysis. The company's security…
- A company is setting up cross-account access for a third-party application. The application runs in an external AWS account (444444444444)…
- 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
- A company governs its multi-account environment with AWS Control Tower. A new internal compliance rule requires that a specific API action…
- A company stores audit logs in a centralized account within AWS Organizations. Security leadership wants to guarantee that no member…
- A solutions architect attaches a service control policy (SCP) to an organizational unit that explicitly allows access to several AWS…
- A company recently migrated to AWS and created several new S3 buckets. The security team wants to ensure that all buckets in the account…
- A company uses AWS Organizations to manage many member accounts. The security team must ensure that individual account administrators…
- 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 withHttpTokens=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
Denyanywhere wins over everyAllow An explicit
Denyin any policy layer short-circuits the whole evaluation[13], so noAllowanywhere 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 explicitDenyon their principal in the resource policy.- 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
OrganizationAccountAccessRoleis 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:
OrganizationAccountAccessRoleis 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:PrincipalOrgIDglobal 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:PrincipalOrgIDgrows with the org.4 questions test this
- A company hosts a data lake in Amazon S3 and needs to restrict bucket access to only members of its AWS organization. The security team…
- A company has a centralized data lake in an S3 bucket that needs to be accessed by applications in over 100 AWS accounts within the same…
- A company has multiple AWS accounts within an AWS Organizations structure. The security team wants to configure an Amazon S3 bucket policy…
- A company is using AWS Organizations with multiple AWS accounts. The company has an S3 bucket in one account that should only be accessible…
- 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
- A company has an S3 bucket in Account A that contains financial reports. Users in Account B need to read these reports. A solutions…
- A company stores data in an Amazon S3 bucket in its own AWS account. A business partner that uses a separate AWS account must be able to…
- A company has two AWS accounts: Account A (production) and Account B (analytics). The analytics team in Account B needs read access to…
- A solutions architect is designing cross-account access for an application in Account A (account ID 111111111111) that needs to write…
- A security engineer needs to enable an application running in AWS account A to access an Amazon S3 bucket in AWS account B. The application…
- A company has two AWS accounts: Account A (111122223333) for production workloads and Account B (444455556666) for analytics. The analytics…
- A company has an S3 bucket in Account A that contains application logs. Users in Account B need read access to objects in this bucket. The…
- A solutions architect needs to grant users in a different AWS account (Account B) the ability to read objects from an S3 bucket owned by…
- Require recent MFA on a role with aws:MultiFactorAuthAge
The
aws:MultiFactorAuthAgecondition 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:MultiFactorAuthPresentinstead: 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
- Official AWS guide to IAM roles
- Configure instance metadata options for existing instances
- Lambda execution role
- Task IAM role (Amazon ECS)
- IAM roles for service accounts (EKS / IRSA)
- AssumeRoleWithWebIdentity: AWS STS API reference
- What is IAM Identity Center?
- Amazon Cognito user pools
- IAM permissions boundaries for IAM entities
- Service control policies (SCPs)
- Using service-linked roles
- How to use trust policies with IAM roles (confused-deputy + ExternalId)
- IAM JSON policy evaluation logic
- Modify the maximum CLI/API session duration for a role
- What is AWS Organizations
- What is IAM Access Analyzer
- Security best practices in IAM