IAM Policy Evaluation and Authorization Strategies on AWS
IAM policy evaluation follows a fixed order: an explicit deny anywhere ends the request; otherwise the request must survive every applicable gate — service control policies, resource control policies, resource-based policies, identity-based policies, permissions boundaries, and session policies — with everything outside an explicit allow denied by default. Task 4.2 of the SCS-C03 exam tests whether you can trace a request through that chain, explain why an action works same-account but fails cross-account, write Deny exceptions and organization perimeters that do not break AWS services, delegate IAM safely with boundaries and paths, design ABAC and RBAC, and authorize applications and outside workloads with Amazon Verified Permissions and IAM Roles Anywhere. It also covers how to find unintended access with IAM Access Analyzer and how to diagnose AccessDenied with the policy simulator, CloudTrail, and decoded messages. Issuing credentials is Task 4.1's job; here we decide what an authenticated principal may do.
On this page17 sections
- The policy evaluation chain: every gate a request must pass
- The five policy types and when to use each
- Same-account OR versus cross-account AND
- A full evaluation trace, gate by gate
- Policy language pitfalls: Not-elements and condition operators
- Writing Deny exceptions: aws:PrincipalArn instead of NotPrincipal
- Condition keys every security engineer must know
- Organization perimeters, service principals and the confused deputy
- SCP and RCP ceilings as they appear in authorization failures
- Delegated administration: permissions boundaries and IAM paths
- ABAC versus RBAC: designing tag-based access
- S3 Block Public Access in request evaluation
- KMS authorization: key policies, grants and cross-account use
- Application authorization with Amazon Verified Permissions
- IAM Roles Anywhere authorization controls
- IAM Access Analyzer: finding and preventing unintended access
- Least-privilege tooling and the AccessDenied triage tree
- Trace any request through the IAM policy evaluation chain, applying explicit denies, SCPs, RCPs, resource policies, boundaries, and session policies in order
- Explain why same-account access needs only one allow while cross-account access must be allowed on both sides, including KMS keys and role trust policies
- Apply condition operators, set operators, and security condition keys correctly — including Deny exceptions on aws:PrincipalArn and perimeters that exempt AWS service principals
- Design delegated administration with permissions boundaries, IAM paths, and scoped iam:PassRole that delegates cannot escape
- Design ABAC with principal, resource, and request tags, and judge when RBAC is the better fit
- Choose authorization controls for applications and outside workloads with Amazon Verified Permissions and IAM Roles Anywhere
- Find and prevent unintended access with IAM Access Analyzer findings, archive rules, and custom policy checks
- Diagnose AccessDenied errors using CloudTrail messages, the IAM Policy Simulator (and its limits), and sts:DecodeAuthorizationMessage
The policy evaluation chain: every gate a request must pass
Every AWS request starts from implicit deny: nothing is permitted until some policy explicitly allows it. Evaluation then applies a fixed sequence, and internalizing it is the single highest-value preparation for this exam.
- Explicit deny check. AWS evaluates all applicable policies of every type first; a
"Effect": "Deny"statement matching the request in any of them ends evaluation immediately. No allow anywhere can override an explicit deny. - Service control policies. If the account belongs to an AWS Organization, the SCPs inherited along its OU path must allow the action. SCPs bound what identities in the account — including the root user of a member account — can ever do.
- Resource control policies. RCPs are the resource-side counterpart to SCPs: organization-managed ceilings on what can be done to supported resources (such as S3 buckets and KMS keys) in member accounts, regardless of who calls.
- Resource-based policy. If the target resource has one, it is evaluated next — and in the same account, its allow can be sufficient by itself.
- Identity-based policies. The policies attached to the calling user, group, or role.
- Permissions boundary. If the identity has one, the action must also be allowed inside the boundary.
- Session policy. If one was passed when the session was created, the action must also fall inside it.
The unifying rule: SCPs, RCPs, permissions boundaries, and session policies never grant anything. They are filters — ceilings the effective permission set is intersected with. Only identity-based and resource-based policies actually grant. When an exam option claims an SCP or boundary "gives" access, it is wrong by definition.
The five policy types and when to use each
Choosing the right policy type for a control objective is a recurring exam pattern. The capability matrix:
| Policy type | Attached to | Can grant? | Primary use |
|---|---|---|---|
| Identity-based | User, group, or role | Yes | Day-to-day permissions for principals |
| Resource-based | The resource (bucket, key, queue, topic, secret, table, role trust) | Yes — including to other accounts | Cross-account access; per-resource control that travels with the resource |
| Permissions boundary | User or role | No — ceiling only | Capping what delegated admins can grant |
| SCP / RCP | Organization root, OU, or account | No — ceiling only | Organization-wide guardrails on principals (SCP) or resources (RCP) |
| Session policy | Passed at AssumeRole, federation, or Roles Anywhere session time | No — narrows the session | Handing a broad role to a caller in scoped-down form |
Not every service supports resource-based policies. S3, KMS, Lambda, SQS, SNS, Secrets Manager, API Gateway, DynamoDB (tables and streams), and IAM role trust policies are the ones the exam leans on. EC2 instances have none, so cross-account access to them always routes through role assumption.
A session policy is the per-session ceiling: the effective permissions of the session are the intersection of the role's identity policies, its boundary (if any), and the session policy. One broad role can therefore serve many callers, each receiving a narrower session. The permissions boundary is the per-identity ceiling and earns its place through delegated administration — letting teams create their own roles that can never exceed an approved maximum. The full pattern, including the denies that stop delegates from removing the boundary, has its own section below.
Same-account OR versus cross-account AND
This distinction decides more exam questions than any other single fact. Within one account, an identity-based allow or a resource-based allow is sufficient (assuming no deny and no failed ceiling). If the bucket policy on my-bucket allows a same-account role to call s3:GetObject, the role needs no S3 permissions in its own identity policy at all — and vice versa. The two grant paths are evaluated as a union.
Across accounts, the logic flips to an intersection: the resource's account must allow the access in a resource-based policy, and the caller's account must allow it in the caller's identity-based policy. Trust must exist on both sides of the account boundary. When something "works from the same account but fails cross-account," the cause is almost always a missing grant on one side.
Refinements that matter at Specialty level:
- Account principal versus a named principal. A resource policy that grants to another account's account principal (
arn:aws:iam::111122223333:root) delegates to the whole account — individual principals there still need identity-policy allows, which that account's admins control. - Which ARN a same-account grant names changes how ceilings apply. A resource policy that grants to a role ARN is still limited by an implicit deny in that role's permissions boundary or session policy. A grant that names an IAM user ARN or a specific assumed-role session ARN is not limited by an implicit deny in the boundary or session policy — the resource-policy allow stands on its own. Explicit denies still win in every case.
- Resource policy or role assumption. Where a service has no resource policy, the caller assumes a role inside the resource's account and everything becomes a same-account evaluation. Where it has one, either works. DynamoDB now supports resource-based policies: a table's policy covers the table and its indexes, while a stream takes its own resource-based policy. Cross-account access still needs the caller's identity policy as well.
Role trust policies are the resource-based policy of a role: they decide who may call sts:AssumeRole on it. For a cross-account assumption, the trust policy in the role's account must name the caller (or the caller's account), and the caller's identity policy in its own account must allow sts:AssumeRole on that role ARN. One subtlety: when a trust policy's Principal names a specific role ARN, IAM binds it to that role's unique ID. If the role is deleted and re-created with the same name, the trust no longer matches, and the policy displays the old unique ID (beginning AROA) instead of the ARN. To trust a role by name, name the account principal and add a condition such as ArnEquals on aws:PrincipalArn for the role ARN.
A full evaluation trace, gate by gate
Walk one request through every gate. A data analyst assumes the role AnalystRole in account 111122223333, which carries a permissions boundary allowing only s3:* and athena:*. The role's identity policy allows s3:GetObject on arn:aws:s3:::finance-data/*. The automation that created the session passed a session policy allowing s3:GetObject only on finance-data/reports/*. The account sits in an OU whose SCP allows all actions except a deny on S3 outside approved Regions. The bucket finance-data is in the same account, with a bucket policy that denies requests where aws:SecureTransport is false. The analyst requests s3:GetObject on finance-data/reports/q3.csv over HTTPS.
- Explicit denies: the bucket policy's
SecureTransportdeny does not match (the request is HTTPS); the SCP's Region deny does not match (the bucket is in an approved Region). No deny fires. - SCP: allows the action. Pass.
- Resource-based policy: the bucket policy grants nothing here — it only denies. No grant from this gate, but no block either.
- Identity-based policy: allows
s3:GetObjectonfinance-data/*, which covers the key. Grant found. - Permissions boundary:
s3:*covers the action. Pass. - Session policy:
reports/*covers the key. Pass.
Result: allowed. Now mutate it: request finance-data/raw/dump.csv and the session policy gate fails — AccessDenied even though the identity policy and boundary both allow it. Request over plain HTTP and the explicit deny ends evaluation before any allow is considered. Move the caller to another account and the bucket policy's lack of a grant becomes fatal, because cross-account requires both sides. One scenario, four different outcomes — this is exactly how the exam varies a single stem.
Policy language pitfalls: Not-elements and condition operators
The policy grammar's sharp edges are exam currency. NotAction with Allow grants everything except the listed actions — far broader than intended if you meant "allow these few things"; its safe use is inside a Deny (deny everything except approved actions) or paired with conditions. NotResource carries the same inversion risk. NotPrincipal is covered in the Deny-exceptions section next.
Operators decide whether a wildcard is a wildcard.
| Operator | Wildcards? | Behaviour |
|---|---|---|
StringEquals | No | Exact, case-sensitive match; * is a literal character |
StringEqualsIgnoreCase | No | Exact match that ignores case; still literal |
StringLike | Yes (*, ?) | Pattern match, case-sensitive; an over-broad pattern (for example a bare * in an account or ARN position of a trust or bucket policy) widens access and is itself a finding |
ArnEquals / ArnLike | Yes, both | The two behave identically: ARN match, segment by segment, with wildcards accepted by either |
Null | — | Tests whether a key is present ("true" = absent) |
...IfExists suffix | — | Treats an absent key as a match |
The Null operator tests key presence: "Null": {"aws:TokenIssueTime": "true"} matches requests made with long-term credentials, because temporary-credential requests carry that key.
How a condition block combines. Several values listed for one key are ORed — the condition matches if the request matches any of them. Several keys under one operator, and several operators in one block, are ANDed — all must match. Negated operators (StringNotEquals, ArnNotLike) with several values match only when the request value matches none of them, and a negated operator evaluates to true when the key is absent from the request. That last rule is behind several perimeter mistakes covered later.
IfExists for multi-resource actions. Some actions are authorized against several resource types in one call. ec2:RunInstances is checked against the instance, image, subnet, security group, network interface, and volume ARNs; ec2:InstanceType exists only in the context for the instance resource. A StringEquals on ec2:InstanceType in an Allow covering all those resources therefore fails for every non-instance resource and the launch is denied. Use StringEqualsIfExists, or put the condition in a statement scoped to instance/* only.
The multivalue trap. When a context key carries multiple values (like aws:TagKeys or aws:PrincipalOrgPaths), use set operators. ForAnyValue matches if at least one value matches; ForAllValues matches if every value matches — and it matches vacuously when the key is absent or empty. AWS warns against ForAllValues in an Allow for exactly this reason: a request that omits the key sails through. If the intent is "the key must be present and every value approved," pair ForAllValues with a Null check. Set operators belong only on multivalued keys; on single-valued keys they can match more than intended, and IAM Access Analyzer policy validation flags that usage.
Writing Deny exceptions: aws:PrincipalArn instead of NotPrincipal
The reliable way to write "deny everyone except these principals" is a Deny with "Principal": "*" and a condition on aws:PrincipalArn — not NotPrincipal. The difference comes from how sessions are identified.
- A role is never the caller; a role session is. Its principal is the assumed-role session ARN,
arn:aws:sts::111122223333:assumed-role/AppRole/session-name. ANotPrincipalexception that lists only the role ARN does not match the session, so the session is denied. Exempting a role withNotPrincipalmeans listing the session ARN (whose session name changes) and the account — brittle by design. Worse, aDenywithNotPrincipalalways denies any IAM user or role that has a permissions boundary, whateverNotPrincipallists, so the exception silently fails for bounded principals. aws:PrincipalArnresolves to the role ARN (including its path) for any session of that role, so one condition covers every session.aws:useridcarries the role's unique ID plus the session name (AROAEXAMPLEID:session-name). It is useful for matching a specific session or tolerating re-creation; it is not a substitute for the ARN.
Several exceptions belong in one statement. Put every exempted principal as multiple values of one ArnNotEquals or ArnNotLike on aws:PrincipalArn. Because a negated operator matches only when the value matches none of the list, the statement denies everyone except the listed principals. Splitting the exceptions into two separate Deny statements breaks it: the statement that exempts principal A still denies principal B, and the one that exempts B denies A — so both end up denied.
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::payroll", "arn:aws:s3:::payroll/*"],
"Condition": {"ArnNotLike": {"aws:PrincipalArn": [
"arn:aws:iam::111122223333:role/PayrollApp",
"arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_PayrollAdmin_*"
]}}IAM Identity Center roles. A permission set provisions a role in each assigned account with an ARN of the form arn:aws:iam::<account>:role/aws-reserved/sso.amazonaws.com/<Region>/AWSReservedSSO_<PermissionSetName>_<suffix>. Instances in us-east-1 have no Region segment. The suffix is generated and changes if the role is re-created, so match these roles with a trailing wildcard (and a wildcard before the role name if you need to cover both path shapes), as in the example above.
Condition keys every security engineer must know
A handful of global condition keys carry most of the security weight on this exam.
aws:PrincipalOrgID— in a resource policy, matches any IAM principal from your AWS Organization. "Allow access only from accounts in our organization" has this one answer, and it beats maintaining account-ID lists forever. It is absent when the caller is an AWS service principal.aws:PrincipalOrgPaths— the OU-level version, for "only principals from accounts under this OU."aws:SourceArn/aws:SourceAccount/aws:SourceOrgID— the confused-deputy defense for service principals. When SNS, S3, or EventBridge invokes your Lambda function or writes to your bucket, the service acts as a deputy; without these conditions, anyone who can point that service at your resource inherits its access. Conditioning on the expected source resource, account, or organization pins the deputy to resources you own.aws:SourceOrgID(and the multivaluedaws:SourceOrgPaths) scales this to every account in the organization without listing them.aws:SourceIpandaws:SourceVpce— network provenance. A Deny on a bucket whoseaws:SourceVpceis not the approved VPC endpoint (or whoseaws:SourceVpcis not the approved VPC) is the standard network-perimeter control for data stores.aws:SourceIpsees the caller's public IP and does not match traffic that reaches the service through a VPC endpoint.aws:SecureTransport— deny whenfalseto refuse plaintext HTTP.aws:PrincipalTag/aws:ResourceTag/aws:RequestTag— the ABAC keys, covered in the ABAC section.aws:PrincipalIsAWSService,aws:ViaAWSService,aws:CalledVia— tell a service principal's own call apart from a service acting with your credentials; the next section shows why that matters.
Read every exam condition twice for direction: a Deny with StringNotEquals on aws:PrincipalOrgID and an Allow with StringEquals achieve related but different postures — the deny blocks outsiders even when another statement would allow them.
Organization perimeters, service principals and the confused deputy
An organization perimeter must handle three kinds of caller differently: IAM principals in your organization, IAM principals outside it, and AWS service principals. Most perimeter bugs come from treating the third like the second.
Scoping to an OU. aws:PrincipalOrgPaths is multivalued, so it needs a set operator, and it needs StringLike with a trailing /* to include child OUs: "ForAnyValue:StringLike": {"aws:PrincipalOrgPaths": "o-a1b2c3d4e5/r-ab12/ou-ab12-11111111/*"}. StringEquals on the exact path matches only principals in that OU itself. The resource-side twin, aws:ResourceOrgID, belongs in identity policies or SCPs when the goal is "our principals may only reach resources owned by our organization."
The service-principal blind spot. A bucket policy that denies when StringNotEquals aws:PrincipalOrgID also denies CloudTrail, AWS Config, and other services writing to the bucket. A service principal carries no aws:PrincipalOrgID, and a negated operator on a missing key is true. Exempt service principals in the same Deny with "BoolIfExists": {"aws:PrincipalIsAWSService": "false"}, and scope the separate service grant with aws:SourceAccount or aws:SourceOrgID so the exemption cannot be borrowed by another customer's resources.
Confused deputy at organization scale. SCPs govern IAM principals in member accounts, not service principals, so they cannot enforce this. A resource control policy can. AWS documents an RCP that denies a service principal acting on behalf of a resource outside the organization:
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:*", "sqs:*", "kms:*", "secretsmanager:*", "sts:*"],
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {"aws:SourceOrgID": "o-a1b2c3d4e5"},
"Null": {"aws:SourceAccount": "false"},
"Bool": {"aws:PrincipalIsAWSService": "true"}
}Read it as three ANDed tests: the caller is a service principal, the request carries source context (so it is acting for a specific resource), and that resource is not in your organization.
Network denies and services acting for you. Several services call other services with the original caller's credentials — Athena reading S3, S3 calling KMS for SSE-KMS. Those downstream calls come from AWS networks, not the caller's IP, so a Deny on NotIpAddress aws:SourceIp breaks them. Add "Bool": {"aws:ViaAWSService": "false"} so the Deny applies only to direct calls.
| Key | True when | Typical use |
|---|---|---|
aws:PrincipalIsAWSService | The caller is a service principal (for example cloudtrail.amazonaws.com writing logs) | Exempting or isolating service-principal calls in perimeters and RCPs |
aws:ViaAWSService | A service makes the request using an IAM principal's credentials on that principal's behalf | Keeping network-based denies from blocking service-to-service calls |
aws:CalledVia | The request passed through the listed service(s) | Allowing an action only through a specific service, such as Athena |
Delegated administration: permissions boundaries and IAM paths
Delegated administration lets an application team create its own roles while nothing it creates can exceed an approved ceiling. A permissions boundary supplies the ceiling; conditions, denies, and IAM paths stop the team from escaping it. Each piece closes a specific gap.
- Require the boundary on creation. Allow
iam:CreateRoleonly with"StringEquals": {"iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/TeamABoundary"}. Attach the same condition to the policy-management actions on the team's roles (AttachRolePolicy,PutRolePolicyand their detach/delete counterparts). - Put the same condition on
iam:PutRolePermissionsBoundary. That action replaces a boundary in one call. Without the condition, a delegate can swap the boundary for a broader policy without ever deleting it. - Deny removing the boundary. Deny
iam:DeleteRolePermissionsBoundary(andiam:DeleteUserPermissionsBoundaryif users are delegated). - Deny editing the boundary policy itself. Deny
iam:CreatePolicyVersion,iam:SetDefaultPolicyVersion,iam:DeletePolicyVersion, andiam:DeletePolicyon the boundary policy's ARN. Otherwise a delegate could widen the ceiling for every role at once. Protect the delegate's own admin role and its policies the same way. - Scope
iam:PassRoleto the team's own roles. The request context forPassRolehasiam:PassedToServiceandiam:AssociatedResourceArnbut notiam:PermissionsBoundary, so you cannot require a boundary there. Instead limit bothCreateRoleandPassRoleto the same resource pattern — the team's IAM path — and restrictiam:PassedToServiceto the services the team uses. The team can then pass only roles it created, and every one of those carries the boundary.
IAM paths are the organising tool. A path is part of a role, user, or policy ARN: arn:aws:iam::111122223333:role/team-a/orders-api. Grant IAM administration and PassRole on role/team-a/* and a team can manage only roles it created under that path. Paths work cleanly in identity policies. In resource policies, two limits apply:
- The
Principalelement does not accept partial wildcards, so you cannot writerole/team-a/*there. - Assumed-role session ARNs omit the path (
arn:aws:sts::111122223333:assumed-role/orders-api/session).
To match roles by path in a resource policy, use "Principal": "*" (or the account principal) with "ArnLike": {"aws:PrincipalArn": "arn:aws:iam::111122223333:role/team-a/*"} — aws:PrincipalArn carries the role's full ARN, path included.
ABAC versus RBAC: designing tag-based access
RBAC maps job functions to roles or permission sets; access changes mean editing policies or assignments. ABAC writes one policy that compares attributes at request time — the canonical pattern conditions on "StringEquals": {"aws:ResourceTag/project": "${aws:PrincipalTag/project}"}, so a principal tagged project=phoenix reaches only resources tagged project=phoenix. New project? Tag the people and the resources; no policy edits. ABAC scales where projects and teams are added quickly and role counts would otherwise explode. RBAC stays the better fit when permissions differ structurally per role rather than by attribute value, when services lack tag-based authorization support, and for coarse multi-account access through Identity Center permission sets. Real designs are hybrids: RBAC for the shape of access, ABAC for its scope.
Where each condition lives. The comparison of aws:ResourceTag with aws:PrincipalTag that controls access to resources belongs in the permissions policy (identity- or resource-based). A role's trust policy governs only who may assume the role and which session tags they may pass; a resource-tag condition there controls nothing about resource access.
Three tag keys, three jobs.
| Key | Describes | Use it to |
|---|---|---|
aws:PrincipalTag/<key> | Tags on the calling principal or its session | Identify the caller's attribute |
aws:ResourceTag/<key> | Tags already on the existing resource | Match the caller to resources it may touch |
aws:RequestTag/<key>, aws:TagKeys | Tags being set in this request | Control which tags may be applied on create or tag calls |
ABAC on creation. A resource being created has no tags yet, so aws:ResourceTag is absent and a condition on it cannot authorize the create. Use aws:RequestTag and aws:TagKeys to require the right tags in the request. In EC2, tagging during RunInstances also needs ec2:CreateTags; scope that permission with "StringEquals": {"ec2:CreateAction": "RunInstances"} so it can be used only as part of the launch, not to retag existing resources.
Tag governance is the security control. If users can retag resources or set their own session tags, they can rewrite their own access. Restrict tag-modification actions, and when a role accepts session tags, allow sts:TagSession in its trust policy only when each passed tag equals the caller's own attribute, for example "StringEquals": {"aws:RequestTag/project": "${aws:PrincipalTag/project}"}. A caller can then only carry its own project value into the session.
S3 Block Public Access in request evaluation
S3 Block Public Access (BPA) is four independent settings, configurable at the organization level (through AWS Organizations S3 policies applied to the root, an OU or an account), the account level, the bucket level and the access point level; the most restrictive combination applies. Two act on ACLs and two act on policies, and only some of them affect what already exists.
| Setting | What it does | Effect on existing configuration |
|---|---|---|
BlockPublicAcls | Rejects requests that set a public ACL on a bucket or object | None — existing ACLs stay as they are |
IgnorePublicAcls | S3 ignores all public ACLs on the bucket and its objects | Yes — existing public ACLs stop granting access |
BlockPublicPolicy | Rejects a bucket (or access point) policy that would grant public access | None — an already-saved public policy is untouched |
RestrictPublicBuckets | If the bucket's policy is public, only AWS service principals and authorized users in the bucket owner's account can access the bucket | Yes — it neutralises an existing public policy |
So the ACL settings affect ACLs only, and when a bucket already has a public policy, only RestrictPublicBuckets changes what that policy grants. With modern buckets, ACLs are usually disabled by the bucket-owner-enforced Object Ownership setting, which leaves the bucket policy as the single authorization surface.
"Public" is decided for the whole policy. S3 judges the bucket policy as a unit: one statement that grants to "Principal": "*" without a restricting condition makes the entire policy public. With RestrictPublicBuckets on, access is then limited to the owner account and service principals — so legitimate cross-account grants elsewhere in the same policy stop working too. A policy is non-public when every grant is to fixed principals, or is conditioned on fixed values of keys such as aws:SourceArn, aws:SourceAccount, aws:SourceVpc, aws:SourceVpce, or a bounded set of aws:SourceIp ranges. Because RestrictPublicBuckets keys on whether the policy as a whole is public, restoring named grants is a matter of making every statement non-public; the setting itself is working as designed.
Access points give each consumer its own named endpoint and policy, so one busy bucket's policy does not become an unmaintainable wall of statements; BPA applies to access point policies too.
IAM Access Analyzer: finding and preventing unintended access
IAM Access Analyzer finds unintended permissions after they exist and stops them before they deploy. Its jobs split into analyzers that produce findings and on-demand policy checks.
| Capability | Question it answers |
|---|---|
| External access analyzer | Which resources are shared outside the zone of trust (the account or the organization)? |
| Internal access analyzer | Which principals inside the organization or account can reach selected critical resources — S3 buckets and directory buckets, DynamoDB tables and streams, RDS DB and cluster snapshots? It combines identity policies, resource policies, SCPs, RCPs, and permissions boundaries. |
| Unused access analyzer | Which roles, access keys, passwords, and permissions have gone unused over a tracking period? |
| Policy generation | What least-privilege policy matches the actions CloudTrail recorded for a role? |
Policy validation (ValidatePolicy) | Does this policy have grammar errors, security warnings, or best-practice problems? |
| Custom policy checks | Does this policy meet my security standard? |
Custom policy checks run in CI/CD pipelines and return pass or fail:
CheckAccessNotGranted— fails if the policy allows any of the named actions (or access to named resources). Use it for "no policy may grantiam:PassRoleors3:DeleteBucket."CheckNoNewAccess— compares the new policy with a reference policy (often the currently deployed version) and fails only if it grants something the reference does not. Access that already existed in the reference passes, so it catches widening, not existing over-permission.CheckNoPublicAccess— fails if a resource policy for a given resource type would allow public access.
Archive rules keep external access findings meaningful. When sharing is intended — a known partner account, a resource type meant to be shared — an archive rule filters on finding attributes such as the principal's account or the resource type and archives matching findings automatically, current and future. New, unexpected sharing still raises an active finding. For KMS keys, remember the analyzer reports access through grants as well as the key policy.
Least-privilege tooling and the AccessDenied triage tree
Least privilege is a process: generate or write a policy, check it before deployment, then prune what goes unused. When a request fails, use tools that explain a denial — not inventory tools. The IAM credential report (credential status per user), last-accessed information (when services and actions were last used), and Access Analyzer policy validation (grammar and best practice) are useful for cleanup but neither record nor explain a live denial.
The IAM Policy Simulator tests "would this principal be allowed to do X?" without making real calls. It evaluates identity policies, permissions boundaries, SCPs (it reports a denial by Organizations without showing the matching statement), and resource policies you supply. It does not evaluate RCPs, and its results can differ from live behaviour for VPC endpoint policies and role chaining. A simulator "allowed" is therefore not proof that the live request will succeed.
When AccessDenied strikes, triage in order rather than guessing:
- Identify the triple. Which principal, which action, which resource? The CloudTrail event's
errorCodeand message usually name all three. - Read the message. Many services now name the policy type: "with an explicit deny in a service control policy," "with an explicit deny in a resource control policy," "because no identity-based policy allows," "because no session policy allows," or "because no VPC endpoint policy allows" (the endpoint's policy, not the bucket or role, is missing the grant). Some messages include the ARN of the denying policy. An SCP denial points to the caller's account path; an RCP denial points to the resource owner's.
- Hunt explicit denies across every layer: SCPs and RCPs, the resource policy, boundaries, identity policies.
- Same account? Verify at least one side — the identity policy or the resource policy — grants the action. Cross-account? Verify both sides grant — and for SSE-KMS, the key policy and caller's identity policy too.
- Check the ceilings: a permissions boundary or session policy (including a Roles Anywhere profile policy) missing the action is the classic "the policy clearly allows it" mystery.
- Check conditions: wrong VPC endpoint, missing tag or MFA context, HTTP instead of HTTPS, a condition key absent for this resource type.
- Decode when offered. EC2 and some other services return
UnauthorizedOperationwith an encoded authorization failure message.sts:DecodeAuthorizationMessagereveals the evaluated context — and the person decoding needs that permission.
Then reproduce the decision in the policy simulator (remembering what it cannot see) before you change a policy.
Tip. This task is tested through evaluation scenarios: a stem hands you an SCP or RCP, a resource policy, an identity policy, and a boundary or session policy, then asks why a request failed or which change fixes it. Expect near-twin choices between condition operators (StringEquals versus StringLike, ForAllValues versus ForAnyValue, IfExists), between NotPrincipal and aws:PrincipalArn Deny exceptions, and between the four Block Public Access settings. Perimeter questions test what happens to AWS service principals, delegation questions test whether a boundary can be removed or bypassed with PassRole, and newer services appear as design choices: Verified Permissions for application authorization, Roles Anywhere trust-policy conditions, and Access Analyzer internal access findings and custom policy checks. SCPs, RCPs, boundaries, and session policies never grant, and an explicit deny beats every allow.
- Evaluation order: explicit deny first, then the organization ceilings, resource-based and identity-based grants, permissions boundaries, and session policies — with implicit deny for anything never allowed.
- Only identity-based and resource-based policies grant access; SCPs, RCPs, boundaries, and session policies are ceilings that can only take away.
- Same-account access needs one allow; cross-account access needs both the resource policy and the caller's identity policy — and for SSE-KMS, the key policy and the caller's kms:Decrypt permission too.
- Exempt principals from a Deny with one statement listing them all under ArnNotLike on aws:PrincipalArn; NotPrincipal misses assumed-role sessions.
- Perimeter Denies on aws:PrincipalOrgID also hit service principals unless you exempt aws:PrincipalIsAWSService; network Denies need aws:ViaAWSService false; organization-wide confused-deputy protection is an RCP on aws:SourceOrgID.
- Delegated administration needs the iam:PermissionsBoundary condition on CreateRole and PutRolePermissionsBoundary, denies on removing or editing the boundary, and PassRole limited to the team's IAM path.
- ForAllValues matches vacuously when the key is absent — pair it with a Null check; use IfExists for keys present on only some resource types of an action.
- RestrictPublicBuckets is the only Block Public Access setting that changes what an existing public bucket policy grants; one unconditioned Principal * statement makes the whole policy public.
- Verified Permissions authorizes inside your applications with Cedar (default deny, forbid wins); Roles Anywhere separates workloads through trust-policy conditions on certificate attributes.
- The IAM Policy Simulator evaluates SCPs but not RCPs; Access Analyzer custom policy checks block risky policies before deployment.
Frequently asked questions
What is the IAM policy evaluation order?
AWS first checks every applicable policy for an explicit deny, which ends evaluation immediately. The request must then be allowed by the organization's SCPs and RCPs where they apply, and by at least one granting policy — resource-based or identity-based — and must also fall inside the permissions boundary and session policy if either exists. Anything not explicitly allowed is implicitly denied.
Does an explicit deny always override an allow in AWS?
Yes. An explicit deny in any applicable policy — identity-based, resource-based, SCP, RCP, permissions boundary, or session policy — overrides every allow, with no exceptions. That is what makes deny statements the right tool for guardrails: a deny with a condition, such as denying requests where aws:SecureTransport is false, holds no matter what other policies grant.
Why does cross-account access fail when the same policy works within one account?
Within one account, an allow from either the identity-based policy or the resource-based policy is sufficient. Across accounts, both are required: the resource owner must grant the caller (or its account) in a resource-based policy, and the caller's own identity policy must allow the action. If access works same-account but fails cross-account, one side — usually the resource policy — is missing its grant.
What is a permissions boundary and when should you use one?
A permissions boundary is a managed policy attached to a user or role that sets the maximum permissions that identity can have — its effective access is the intersection of the boundary and its identity policies, and the boundary itself grants nothing. Use it to delegate IAM administration safely: allow CreateRole and PutRolePermissionsBoundary only with the iam:PermissionsBoundary condition naming the approved boundary, deny deleting the boundary or editing the boundary policy, and limit iam:PassRole to roles under the team's IAM path.
What is the difference between ABAC and RBAC in AWS?
RBAC grants access through roles or permission sets mapped to job functions, so changing access means changing policies or assignments. ABAC grants access through attributes — typically a condition matching aws:PrincipalTag against aws:ResourceTag — so one policy scales across projects and new access is granted by tagging. ABAC suits large, dynamic environments with strong tag governance; RBAC suits structurally distinct job functions and services without tag-based authorization.
What does S3 Block Public Access RestrictPublicBuckets do?
RestrictPublicBuckets limits access to a bucket whose policy is public to AWS service principals and authorized users in the bucket owner's account. It is the only Block Public Access setting that changes what an already-saved public policy grants: BlockPublicPolicy only rejects new public policies, and the two ACL settings affect ACLs only. Because S3 judges the whole policy, one unconditioned Principal * statement makes it public, and named cross-account grants in the same policy then stop working too.
When should I use Amazon Verified Permissions instead of IAM?
Use IAM to authorize calls to AWS APIs and Amazon Verified Permissions to authorize decisions inside your own application, such as whether a user may view or share a record. Verified Permissions stores Cedar policies and a schema in a policy store, evaluates them with default deny and forbid-overrides-permit, and answers IsAuthorized (application-supplied principal) or IsAuthorizedWithToken (principal from a Cognito or OIDC token) requests.
How do I troubleshoot an AccessDenied error in AWS?
Pin down the exact principal, action, and resource from the CloudTrail event and read the message, which often names the policy type that denied — for example a service control policy, a resource control policy, a session policy, or a VPC endpoint policy. Hunt explicit denies, confirm both sides grant for cross-account calls, and check boundaries, session policies, and conditions. Decode EC2 UnauthorizedOperation messages with sts:DecodeAuthorizationMessage, and remember that the IAM Policy Simulator evaluates SCPs but not RCPs.
Source
This lesson covers the "Identity and Access Management" domain of the official SCS-C03 exam guide. Vendors revise their guides — check the source for the current version.
- AWS Certified Security – Specialty (SCS-C03) exam guide — Amazon Web Services
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.