Course outline

20.2

Conditions, Resource Policies, Boundaries and Full Evaluation Logic

Condition keys, S3 bucket vs object ARNs, roles vs resource policies, permission boundaries, and the complete decision flow.

28 min Reading + animations

Tip: tap or hover any dotted-underlined term to see what it means.

By the end of this lesson you will be able to

  • Read and write any Condition block: operators, keys, values, how several keys and several values combine, and what happens when a key is missing.
  • Use the global condition keys the exam asks about (aws:SourceIp, aws:RequestedRegion, aws:MultiFactorAuthPresent, aws:PrincipalOrgID, tag keys, aws:SecureTransport) and the service keys ec2:ResourceTag, s3:prefix and iam:PermissionsBoundary, and know where each one breaks.
  • Build attribute-based access control (ABAC) with tags, and protect the tags themselves.
  • Write S3 policies that pair bucket-level and object-level actions with the right ARN.
  • Choose between an IAM role and a resource-based policy for cross-account access, and explain why assuming a role gives up your own permissions.
  • Use permissions boundaries to let developers create roles safely, and know how boundaries interact with SCPs, session policies and resource-based policies.
  • Evaluate any request by hand, in one account or across two accounts, including RCPs, SCPs, boundaries and session policies, and test a policy before it reaches production.

#Where lesson 2.2 left off

Lesson 2.2 taught the three rules every request obeys: everything starts denied, an explicit Allow lifts the default, and an explicit Deny beats every Allow. The owner’s notes open with the example that proves it: a user in Group A (Allow S3 read and write) and Group B (Deny S3) is denied, because groups have no priority and one matching Deny wins; if Group B merely says nothing about S3, Group A’s Allow works. Lesson 2.2 also listed the six policy types, walked the single-account flow and previewed SCPs and boundaries.

This lesson goes deeper on the four things that turn that simple picture into real exam questions:

  1. Conditions: the part of a policy that decides when and how a statement applies.
  2. Resources: S3’s bucket-versus-object trap, and resource-based policies with aws:PrincipalOrgID.
  3. Roles versus resource-based policies: two ways across an account boundary that behave very differently.
  4. Limits: permissions boundaries, session policies, and how SCPs and RCPs (whose organization structure lesson 20.1 builds) fit into the complete evaluation flow, in one account and across two.

#Conditions in depth

The notes call the Condition block the most powerful part of a policy, and they are right. Users and resources say who may do what; conditions say when and how: only from the office network, only with MFA, only in one Region, only on resources tagged for your team.

#Anatomy: operator, key, values

Every condition has the same three parts (lesson 2.1 introduced them):

"Condition": {
  "StringEquals": { "aws:RequestedRegion": ["eu-west-1", "ap-south-1"] },
  "Bool":         { "aws:SecureTransport": "true" }
}
  • The operator (StringEquals, Bool) says how to compare.
  • The key (aws:RequestedRegion) names a fact in the request context: the bundle of facts AWS collects about every call (who, from where, when, with what credentials, on which resource, with which tags).
  • The values are what the fact is compared against.

The combining rules fit in one table:

Inside a Condition blockLogicExample
Several operatorsANDRegion matches and TLS was used
Several keys under one operatorAND"StringEquals": {"aws:RequestedRegion": "eu-west-1", "aws:PrincipalTag/team": "data"}
Several values for one keyORRegion is eu-west-1 or ap-south-1
Several statementseach is evaluated on its ownone Allow statement for the office, another for the VPN

If you need “A or B” across two different keys (from the office or with MFA), you cannot write it in one condition block: write two statements.

#The operators

FamilyOperatorsTypical keysWatch out for
StringStringEquals, StringNotEquals, StringEqualsIgnoreCase, StringLike (*, ?), StringNotLikeaws:RequestedRegion, aws:PrincipalTag/team, s3:prefixCase-sensitive unless IgnoreCase; wildcards only with Like
NumericNumericEquals, NumericLessThan, NumericGreaterThanEquals…aws:MultiFactorAuthAge, s3:max-keysValues are written as strings: "3600"
DateDateLessThan, DateGreaterThan…aws:CurrentTime, aws:TokenIssueTimeISO 8601 or epoch time
BooleanBoolaws:SecureTransport, aws:MultiFactorAuthPresent, aws:ViaAWSServiceA missing key is not false
IP addressIpAddress, NotIpAddressaws:SourceIpPublic addresses only; missing through VPC endpoints
ARNArnEquals, ArnLike, ArnNotLikeaws:SourceArn, aws:PrincipalArnEach ARN part is matched separately
ExistenceNullany key"Null": {"aws:TokenIssueTime": "true"} means “the key is absent”
Modifier…IfExists on any operator except Nullkeys that are present only sometimesA missing key counts as a match
Set operatorsForAnyValue:, ForAllValues: before an operatormulti-valued keys: aws:TagKeys, aws:CalledVia, aws:PrincipalOrgPathsRequired for multi-valued keys; never for single-valued ones

#When the key is missing

This rule explains most surprising condition results, so learn it word for word. From the IAM User Guide:

  • A normal operator with a missing key is false. IpAddress on a request with no aws:SourceIp does not match.
  • A negated operator (NotIpAddress, StringNotEquals, ArnNotLike…) with a missing key is true. “The address is not the office” is considered true when there is no address at all.
  • An …IfExists operator with a missing key is true: “check it if it is there, otherwise don’t worry”.
  • Null tests presence itself: true = absent, false = present.

Combine that with the effect of the statement and you get four behaviours to remember:

StatementKey missingEffect on the request
Allow + normal operatorcondition falsethe Allow does not apply: often an implicit deny
Allow + …IfExistscondition truethe Allow applies
Deny + negated operatorcondition truedenied, often by surprise
Deny + normal operator (Bool false)condition falsethe Deny does not apply: the guard is bypassed

#The notes’ three exam scenarios

The notes give three phrases that should make you think “Condition element”, and the slides show the same keys:

Exam phraseKeyShape of the answer
“Restrict API calls to the corporate IP range”aws:SourceIpDeny with NotIpAddress, or Allow with IpAddress
“Force MFA for deleting an S3 bucket”aws:MultiFactorAuthPresentDeny s3:DeleteBucket with BoolIfExists false (or Allow with Bool true)
“Allow EC2 creation only in eu-west-1”aws:RequestedRegionDeny with StringNotEquals the allowed Region, global services excluded (lesson 2.2’s Region guard)

#aws:SourceIp, VPC endpoints and AWS services

aws:SourceIp holds the public address the request came from. Two common paths do not have one you control:

  • Through a VPC endpoint (lesson 22.2): the key is not in the request context at all. With a Deny + NotIpAddress guard, the negated operator on a missing key is true, so your own private servers are denied. Match them with aws:SourceVpce (the endpoint ID), aws:SourceVpc (the VPC ID) or aws:VpcSourceIp (the private address, always together with one of the other two).
  • An AWS service acting for you (CloudFormation creating resources, Athena reading S3 with your identity): the target service sees the calling service’s address. The IAM docs say plainly: do not use aws:SourceIp for such calls. Requests made this way (forward access sessions) carry aws:ViaAWSService = true and list the services in aws:CalledVia, so add "Bool": {"aws:ViaAWSService": "false"} to the Deny to skip them. For service principals (a service acting as itself, such as CloudTrail writing logs), the matching key is aws:PrincipalIsAWSService.

The fixed office guard therefore looks like this. Read the three conditions as one AND: deny only if the call is not from the office, and not through our endpoint, and not made by a service on our behalf.

{
  "Sid": "DenyOutsideOfficeAndEndpoint",
  "Effect": "Deny",
  "Action": "s3:*",
  "Resource": ["arn:aws:s3:::payroll-example", "arn:aws:s3:::payroll-example/*"],
  "Condition": {
    "NotIpAddress":    { "aws:SourceIp": "203.0.113.0/24" },
    "StringNotEquals": { "aws:SourceVpce": "vpce-0a1b2c3d" },
    "Bool":            { "aws:ViaAWSService": "false" }
  }
}

One IP rule, five ways to call S3

The payroll bucket has a guard: deny every request whose aws:SourceIp is not the Motijheel head-office range 203.0.113.0/24. Watch which calls it lets through, and which ones it blocks by surprise.

Scenario

The normal case: a direct call from the office range.

Head office203.0.113.0/24
Home Wi-Fi198.51.100.7
CloudFormation
EC2 appprivate 10.0.1.25
VPC endpoint
S3 bucketpayroll-example
GetObject

Request context

aws:SourceIp

203.0.113.25

aws:SourceVpce

(missing)

aws:ViaAWSService

false

Step 1 of 2
1

An accountant at head office downloads a payslip file. The request leaves through the office’s public address 203.0.113.25, so AWS puts that address in the request context as aws:SourceIp.

#Bool versus BoolIfExists: missing is not false

Lesson 2.3 showed the MFA table; here is why it works the way it does. aws:MultiFactorAuthPresent exists only for temporary credentials: the console (which uses temporary credentials behind the scenes), STS sessions, and the temporary credentials a service creates when it reuses your identity. For long-term access keys it is simply missing.

  • Deny + "Bool": {"aws:MultiFactorAuthPresent": "false"}: a normal operator on a missing key is false, so every script with an access key walks past the guard. This is the trap.
  • Deny + "BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}: missing counts as a match, so access keys are denied too. AWS recommends this one for “nothing without MFA”.
  • Allow + "Bool": {"aws:MultiFactorAuthPresent": "true"}: only sessions that really used MFA.
  • “Recently” is NumericLessThan on aws:MultiFactorAuthAge, for example "3600" seconds.

Try every combination in the tester below: pick where a request comes from, and watch which keys are present and how six common statements react.

Condition tester: is the key even there?

Pick where a request comes from. The lab lists the condition keys AWS puts in the request context, then evaluates six common statements against it. Watch the missing keys.

Request from

Request context

aws:SourceIp 203.0.113.25aws:SourceVpce (missing)aws:ViaAWSService falseaws:MultiFactorAuthPresent true

Allow: Allow only from the office

  • IpAddress aws:SourceIp 203.0.113.0/24
    203.0.113.25 → true

applies

A positive operator with a missing key is false, so the Allow simply does not apply.

Deny: Deny outside the office (naive)

  • NotIpAddress aws:SourceIp 203.0.113.0/24
    203.0.113.25 → false

does not apply

A negated operator with a missing key is true, so this Deny also hits VPC endpoint traffic and services acting for you.

Deny: Deny outside the office (fixed)

  • NotIpAddress aws:SourceIp 203.0.113.0/24
    203.0.113.25 → false
  • StringNotEquals aws:SourceVpce vpce-0a1b2c3d
    key missing → negated operator is true
  • Bool aws:ViaAWSService false
    false → true

does not apply

Three conditions in one statement combine with AND: the Deny needs “not the office” and “not our endpoint” and “not a service on your behalf”.

Deny: Deny without MFA, Bool (trap)

  • Bool aws:MultiFactorAuthPresent false
    true → false

does not apply

Long-term access keys have no MFA key at all, so Bool false never matches them and they walk straight through.

Deny: Deny without MFA, BoolIfExists

  • BoolIfExists aws:MultiFactorAuthPresent false
    true → false

does not apply

IfExists turns a missing key into a match, so access keys are denied too. This is the guard AWS recommends.

Allow: Allow only with MFA, Bool

  • Bool aws:MultiFactorAuthPresent true
    true → true

applies

Only sessions that really used MFA get this Allow; access keys and roles without MFA do not.

i

Signed in to the console at head office with a passkey. The console uses temporary credentials, so the MFA key is present and true.

#Multi-valued keys: ForAnyValue and ForAllValues

Most keys hold one value per request (one source IP, one Region). A few hold a set: aws:TagKeys (all tag keys in a tagging request), aws:CalledVia, aws:PrincipalOrgPaths. For those you must add a set operator:

  • ForAnyValue:StringEquals: true if at least one value in the request is in your list. False if the key is missing.
  • ForAllValues:StringEquals: true if every value in the request is in your list, and also true if the key is missing or empty.

That last point is a real security trap: Allow + ForAllValues:StringEquals aws:TagKeys ["team", "env"] meant “you may only use these tag keys”, but a request with no tags matches too. AWS’s fix is to add "Null": {"aws:TagKeys": "false"} (the key must be present). The reverse mistake also exists: using a set operator on a single-valued key such as aws:ResourceTag/team can make a policy more permissive than intended.

#Service-specific keys

Every service publishes its own keys, prefixed with the service name. Four that appear on the exam and in real designs:

KeyWhat it holdsTypical use
ec2:ResourceTag/<key>A tag on the EC2 resource being acted on“Start and stop only instances tagged env=dev” (the slides’ example)
s3:prefixThe folder prefix in a ListBucket request“List only home/<your name>/”
iam:PermissionsBoundaryThe boundary ARN in a request that creates or changes a user or roleForce every new role to carry the approved boundary (below)
iam:PassedToServiceThe service a role is being passed toAllow iam:PassRole only to lambda.amazonaws.com

The last one completes lesson 2.4’s iam:PassRole story: limit iam:PassRole to specific role ARNs, and with iam:PassedToService to the one service that should receive them, so nobody can hand an administrator role to a function they control.

#Policy variables

A value can contain a variable that AWS fills in from the request context: ${aws:username}, ${aws:PrincipalTag/team}, ${aws:PrincipalAccount}. One policy then behaves differently for each caller. You will see variables in the S3 home-folder pattern and in ABAC below. Variables need the 2012-10-17 policy version (the old 2008-10-17 treats ${…} as plain text).

#ABAC: permissions from tags

Attribute-based access control grants access by comparing tags instead of listing resources. Four keys make it work:

KeyHoldsRole in ABAC
aws:PrincipalTag/<key>A tag on the calling user or role (or a session tag from the identity provider)Who the caller is: team, project, cost centre
aws:ResourceTag/<key> (or ec2:ResourceTag/<key>)A tag on the existing resourceWhat the resource belongs to
aws:RequestTag/<key>A tag being set in this requestForce new resources to be created with the right tag
aws:TagKeysAll tag keys in this request (multi-valued)Control which tag keys may be added or removed

One statement serves every team:

{
  "Effect": "Allow",
  "Action": ["ec2:StartInstances", "ec2:StopInstances"],
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" }
  }
}

Why ABAC over role-based access control (RBAC): with RBAC each team needs its own policy listing its resources, and every new project means editing policies. With ABAC the payments team’s new instances are covered the moment they are tagged team=payments, and a new team needs no new policy at all. The price: tags become permissions, so changing tags must be controlled as tightly as changing policies (deny CreateTags/DeleteTags on the team key for everyone but the platform team), and new resources must be forced to carry the tag with aws:RequestTag. Lesson 20.3 shows how IAM Identity Center passes attributes from your identity provider as session tags, so a person’s department in the company directory becomes their aws:PrincipalTag.

ABAC: tags decide who touches what

A telecom has one policy for all developers: you may start and stop an instance only if its team tag equals your own team tag. No list of instance IDs anywhere.

Scenario

aws:PrincipalTag/team equals ec2:ResourceTag/team.

Rafiteam=payments
Policy engine
Instance Ateam=payments
Instance Bteam=lending
StopInstances
Step 1 of 3
1

Rafi stops a payments instance at night to save money. His IAM role carries the tag team=payments.

#IAM for S3: bucket ARN versus object ARN

S3 permissions act on two kinds of resource, and the slides make the point with one bucket:

LevelActionsResource ARN
Buckets3:ListBucket, s3:GetBucketLocation, s3:GetBucketPolicy, s3:PutBucketVersioning…arn:aws:s3:::reports-example
Objects3:GetObject, s3:PutObject, s3:DeleteObject, s3:GetObjectTagging…arn:aws:s3:::reports-example/*

A policy with only reports-example/* lets you read files whose names you already know, but ListBucket fails with AccessDenied, because the pattern bucket/* never matches the bucket itself. Granting s3:* does not help; adding the bucket ARN does:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTheBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::reports-example"
    },
    {
      "Sid": "ReadAndWriteObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::reports-example/*"
    }
  ]
}

To give each user a folder in a shared bucket, combine a ListBucket statement with a condition on s3:prefix and object statements on home/${aws:username}/*. The s3:prefix key exists only in list requests, which is exactly why it belongs on the bucket-level statement.

Bucket ARN or object ARN?

S3 actions act on two different resources: the bucket itself and the objects inside it. Watch what happens when a policy names only one of them.

Scenario

The policy allows s3:* on arn:aws:s3:::reports-example/* and nothing else.

Report appIAM role
Policy engine
S3 bucketreports-example
GetObject q3.csv
Step 1 of 5
1

Reading one known file. GetObject acts on the object ARN arn:aws:s3:::reports-example/q3.csv, which matches the pattern bucket/*.

#Resource-based policies and aws:PrincipalOrgID

A resource-based policy lives on the resource and names a Principal (lesson 2.1). Listing partner accounts one by one in a bucket policy breaks the day an account is added. The slides’ answer, and the exam’s, is aws:PrincipalOrgID: allow everyone in your AWS Organization, and nobody else.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "OrgMembersRead",
    "Effect": "Allow",
    "Principal": "*",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": ["arn:aws:s3:::finance-data-example", "arn:aws:s3:::finance-data-example/*"],
    "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-a1b2c3d4e5" } }
  }]
}

"Principal": "*" looks frightening, but the condition narrows it to principals whose account is in o-a1b2c3d4e5; a user outside the organization has no such key and is not allowed. New member accounts are included automatically, removed ones lose access automatically (lesson 10.2 used the same key for bucket policies). The caller’s own identity policy must still allow the call, as in any cross-account request.

Related keys, each for a different question:

KeyAsksTypical use
aws:PrincipalOrgIDIs the caller in this organization?Bucket, queue or key policy open to the whole organization
aws:PrincipalOrgPathsIs the caller under this OU path? (multi-valued)Open a bucket only to the Prod OU: ForAnyValue:StringLike o-a1b2c3d4e5/r-ab12/ou-ab12-cdef3456/*
aws:ResourceOrgIDIs the resource in this organization?SCP or identity policy: “our people may only write to our organization’s buckets” (stops data leaving to a personal bucket)
aws:SourceArn, aws:SourceAccountWhich resource or account made an AWS service act?Stop the confused deputy when a service principal writes to your resource

The last row is the service-principal version of lesson 2.4’s external ID. When a queue policy allows sns.amazonaws.com to send messages, any SNS topic in any account could use that permission. Add "ArnEquals": {"aws:SourceArn": "arn:aws:sns:ap-south-1:111122223333:orders-topic"} (or aws:SourceAccount) and only your topic can.

#IAM roles versus resource-based policies

Both get a principal across an account boundary, as the slides show: account A’s user either assumes a role in account B (the role acts as a proxy), or B attaches a resource-based policy that names the user directly. The notes explain the difference that matters.

#Assuming a role swaps your identity

When a user, application or service assumes a role, it gives up its original permissions for that session and holds only the role’s permissions. Calls are made as assumed-role/<role>/<session>, not as the original user.

#A resource-based policy keeps your identity

With a resource-based policy the caller stays itself, with all its own permissions, and gains access to that one resource.

The slides’ example makes the difference concrete: a user in account A must scan a DynamoDB table in A and write the dump to an S3 bucket in B.

  • With a role in B, the session can write to B’s bucket but can no longer read A’s table: the role in B has no permission there and the user’s own permissions are set aside. The job needs two identities and two steps.
  • With a bucket policy in B naming the user, the user scans A’s table and writes to B’s bucket in one session, as themself.

Assume a role, or be on the guest list?

Nadia in account A must scan a DynamoDB table in account A and write the dump to a bucket in account B. Two ways to give cross-account access behave very differently.

Scenario

The role is a proxy: Nadia gives up her own permissions while she wears it.

Account A
Account B
NadiaIAM user
Orders table
Export roleNadia inside
Dump bucketbucket policy
AssumeRole
Step 1 of 4
1

B’s export role trusts account A, and Nadia’s policy allows sts:AssumeRole on it. She gets temporary credentials for the role (lesson 2.4).

The reverse edge case is just as common: Nadia assumes a role in account B to read a bucket there and copy the files back to her own bucket in A. The copy fails, because in the role she is no longer Nadia; it works only if A’s bucket policy trusts B’s role. Since 2024 DynamoDB also supports resource-based policies, so the slide’s scan could be fixed the same way, by letting the table’s policy allow B’s role.

#Which services support resource-based policies

Only some do, so the choice is not always yours:

Resource-based policy availableExamples of when it is the natural choice
S3 buckets (bucket policy), S3 access pointsShare data across accounts or with the whole organization
SQS queues, SNS topics, EventBridge event busesLet another account or a service send messages or events
Lambda functions (function policy)Let S3, SNS, API Gateway or another account invoke the function
KMS keys (key policy)Every key has one; it must allow access (lesson 21.2)
Secrets Manager secrets, ECR repositories, Backup vaults, DynamoDB tables and streams, API Gateway REST APIs, OpenSearch domains, Glacier vaultsCross-account access to that one resource
IAM roles (the trust policy)Says who may assume the role

Services without one (most EC2 resources, RDS instances, EBS volumes) are shared across accounts with roles, or with AWS Resource Access Manager for networking resources (lesson 20.1). The same split decides how EventBridge gets permission to reach a rule’s target: Lambda, SNS, SQS and the like through their resource-based policies, others through an IAM role, as lesson 19.5 explains.

#Which one to pick

SituationPickWhy
“Access another account’s bucket without losing current permissions” (the notes’ exam phrase)Resource-based policyThe caller keeps its identity and its own permissions
“Provide credentials to an EC2 instance dynamically” (the notes’ other phrase)IAM role (instance profile)Services get temporary credentials only through roles
A third-party vendor needs to work in your accountIAM role with an external IDShort-lived credentials, audit trail, easy to revoke
Access to many resources or services in the other accountIAM roleOne trust, one permission set; listing the user in dozens of resource policies does not scale
The resource type has no resource-based policyIAM roleNo other way
The whole organization must read one bucketResource-based policy with aws:PrincipalOrgIDOne statement, no per-account roles

#Cross-account means both sides must allow

Inside one account, an Allow in the identity-based policy or in the resource-based policy is enough (union). Across accounts, AWS runs two evaluations, one in each account, and the request is allowed only if both return Allow:

  • The caller’s account (the trusted account) checks its identity-based policies and everything that limits them: SCPs, permissions boundary, session policy.
  • The resource’s account (the trusting account) checks the resource-based policy and everything that limits it: RCPs.

So B’s bucket policy naming Nadia is useless if Nadia’s own policy in A does not allow s3:PutObject on that bucket ARN, and a generous identity policy in A is useless if B’s bucket policy does not name A. Two resource-based policies must allow even inside one account, because they decide who may use the resource at all: the role trust policy and the KMS key policy.

#Permissions boundaries in depth

A permissions boundary is a managed policy (AWS managed or customer managed) attached to an IAM user or role as its maximum permissions. It never grants anything itself; the slides draw it as “boundary + IAM policy”, where only the overlap is effective, and with no permission policy at all the result is no permissions.

  • Boundaries apply to users and roles only, not to groups (the notes imply it; the slides state it).
  • Effective permissions = identity-based policies ∩ boundary (and, when present, ∩ SCPs, ∩ RCPs, ∩ session policy).
  • An explicit Deny in the boundary denies, like any Deny.

The notes’ example: a user’s boundary allows only S3 read and EC2 read, while their identity policy is AdministratorAccess. The user can do only S3 read and EC2 read: AdministratorAccess offers everything, the boundary keeps only those two.

#The delegation pattern

The notes give the exam’s favourite scenario: “Developers should be able to create IAM roles, but must not be able to give themselves administrator permissions through those roles.” The answer is to force a boundary on everything they create. The slides list the same use cases: delegate to non-administrators within their boundary, let developers manage their own permissions without escalating, and restrict one specific user without an account-wide SCP.

The developers’ own policy has four parts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CreateRolesOnlyWithTheBoundary",
      "Effect": "Allow",
      "Action": ["iam:CreateRole", "iam:PutRolePermissionsBoundary",
                 "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:PutRolePolicy"],
      "Resource": "arn:aws:iam::111122223333:role/app/*",
      "Condition": {
        "StringEquals": { "iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/DevBoundary" }
      }
    },
    {
      "Sid": "PassOnlyTheirRolesToLambda",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::111122223333:role/app/*",
      "Condition": { "StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" } }
    },
    {
      "Sid": "NeverRemoveABoundary",
      "Effect": "Deny",
      "Action": ["iam:DeleteRolePermissionsBoundary", "iam:DeleteUserPermissionsBoundary"],
      "Resource": "*"
    },
    {
      "Sid": "NeverEditTheBoundaryPolicy",
      "Effect": "Deny",
      "Action": ["iam:CreatePolicyVersion", "iam:SetDefaultPolicyVersion", "iam:DeletePolicy"],
      "Resource": "arn:aws:iam::111122223333:policy/DevBoundary"
    }
  ]
}
  1. They may create and edit roles under the path app/ only when the request names DevBoundary as the boundary. A CreateRole without it has no iam:PermissionsBoundary key, the condition is false, and the request is implicitly denied.
  2. They may pass those roles only to Lambda.
  3. They can never take a boundary off.
  4. They can never edit the boundary policy itself, or they would simply widen it.

Usually the developers carry the same boundary themselves, so they cannot use their IAM rights to do more than the roles they create could.

Developers create roles, but only inside the fence

Tanvir may create IAM roles for his Lambda functions without waiting for the security team, but every role must carry the approved boundary DevBoundary, which allows only S3, DynamoDB and CloudWatch Logs.

Scenario

The allowed path.

Tanvirdeveloper
IAM
New roleorders-fn-role
AWS APIs
CreateRole + DevBoundary
Step 1 of 3
1

Tanvir runs aws iam create-role with --permissions-boundary pointing at DevBoundary.

#Boundary, SCP or session policy?

All three only limit. They differ in what they are attached to and who sets them:

Permissions boundarySCPSession policy
Attached toOne IAM user or roleOrganization root, OU or accountOne session, passed at AssumeRole/federation time
Set byWhoever creates or administers that user or roleThe organization’s administrators (lesson 20.1)Whoever creates the session
ScopeThat one identity, alwaysEvery principal in the account (not the management account)Only that session, until it expires
Typical useDelegation; restrict one developerGuard rails for whole accountsA broker handing out narrower credentials per job

The slides’ point “boundaries can be used in combination with SCPs” is just intersection: an action must be allowed by the identity policy and the boundary and the SCP.

#Boundaries and resource-based policies: the nuance

The IAM User Guide adds a detail many courses get wrong. Within the same account:

  • A resource-based policy that grants access to an IAM user ARN is not limited by an implicit deny in that user’s identity policies or boundary. The bucket’s Allow stands on its own.
  • A resource-based policy that grants access to an IAM role ARN is limited by an implicit deny in the role’s boundary and session policy.
  • A resource-based policy that names a role session ARN directly grants to that session and is not limited by them.
  • An explicit Deny in the boundary always wins.

Across accounts this nuance disappears: the caller’s account must allow the request through identity ∩ boundary ∩ session, whatever the resource policy says.

#Session policies

A session policy is a policy passed when a session is created: aws sts assume-role --policy (inline JSON) or --policy-arns (up to 10 managed policies), and the same parameters on AssumeRoleWithSAML, AssumeRoleWithWebIdentity and GetFederationToken. The session gets the intersection of the role’s (or user’s) policies and the session policy. It can never add anything.

Why use one: a single broad role serves many jobs, and each job receives credentials cut down to what it needs. A data-export service assumes ExportRole (read all buckets) but passes a session policy allowing only s3:GetObject on one customer’s prefix, so a bug in one job cannot read another customer’s files.

#The complete evaluation flow

#In one account

Lesson 2.2 walked the documented order step by step; in short (IAM User Guide, checked November 2026):

  1. Any explicit Deny in any policy type → Deny.
  2. RCPs must allow (in practice RCPFullAWSAccess is always attached, so they stop requests only by Deny).
  3. SCPs must allow (not for the management account).
  4. Resource-based policy allows? For an IAM user ARN that is enough → Allow. For a role ARN, continue but remember the Allow.
  5. Identity-based policies must allow (unless step 4 allowed).
  6. Permissions boundary must allow, if set.
  7. Session policy must allow, if it is a session with one → Allow.

#Across two accounts

For a cross-account request AWS runs the flow twice:

EvaluationPolicies checkedTypical stop
Caller’s account (A)explicit Denies in A’s policies, A’s SCPs, the caller’s identity-based policies, boundary, session policy“no identity-based policy allows”; an SCP in A that keeps data inside A’s organization (aws:ResourceOrgID)
Resource’s account (B)explicit Denies in B’s policies, B’s RCPs, the resource-based policy“no resource-based policy allows”; an RCP in B that rejects principals outside B’s organization

The request is allowed only if both say Allow. B’s SCPs play no part (they limit B’s principals, not A’s), and A’s RCPs play no part (they limit A’s resources).

Across accounts: two checks, both must pass

A partner company’s analytics role in account 444455556666 reads a shared bucket in your account 111122223333. AWS checks the request on the caller’s side and on the resource’s side.

Scenario

The working setup.

Partner role444455556666
Caller sideidentity, SCPAllow
Resource sidebucket policy, RCP
Shared bucket111122223333
GetObject
Step 1 of 4
1

First evaluation, in the caller’s account: any Deny? Do its SCPs allow S3? Does the role’s identity policy allow GetObject on the bucket ARN? Does its boundary or session policy allow it?

#The evaluation lab

Set every layer yourself. Put the bucket in the same account or in another account, choose a user or a role session, require MFA in the identity policy, and switch SCPs, RCPs, the bucket policy, the boundary and the session policy between Allow, silent and Deny. The lab shows each evaluation’s gates and where the request stops.

Evaluation lab: one account or two

A principal asks for s3:GetObject on a bucket. Put the bucket in the same account or in another one, set every policy layer, and see where the request stops. Across accounts the lab runs two evaluations, one per account.

Scenarios
Bucket is in
Caller
Request signed with MFA
Caller’s identity policy
Bucket policy (names the caller)
Caller’s boundary
Session policy
SCP on the caller’s account
RCP on the bucket’s account

Account A: the caller’s side

  1. 1. Any Deny?

    no Deny

  2. 2. SCP

    allows

  3. 3. Identity-based

    an Allow matches

  4. 4. Boundary

    not set

  5. 5. Session

    Allow

Account B: the bucket’s side

  1. 1. Any Deny?

    no Deny

  2. 2. RCP

    full access

  3. 3. Bucket policy

    names the caller: Allow

Decision: Allowed

i

A partner’s analytics role reads your bucket. Its own policy allows the bucket ARN, your bucket policy names the role: both evaluations allow. Both evaluations returned Allow, so the request is allowed. If either had failed, the request would be denied.

#Worked example: the slides’ policy

The last slide asks three questions about one policy. Here is a policy of the same kind:

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Deny",  "Action": "sqs:*", "Resource": "*" },
    { "Effect": "Allow", "Action": "sqs:DeleteQueue",
      "Resource": "arn:aws:sqs:ap-south-1:111122223333:orders-queue" }
  ]
}
  • sqs:CreateQueue? The Deny on sqs:* matches: explicit deny.
  • sqs:DeleteQueue on orders-queue? The Allow matches, but so does the Deny. A Deny is not beaten by being less specific: explicit deny.
  • ec2:DescribeInstances? The Deny is only about SQS and nothing allows EC2: implicit deny.

Self-checkQuestion

An IAM user has this policy and no other: Deny sqs:* on *, and Allow sqs:DeleteQueue on one queue ARN. What happens when the user calls sqs:DeleteQueue on that queue?

#Testing a policy before it hurts

Conditions and boundaries are easy to get subtly wrong. Test them before attaching them:

  • IAM policy simulator (aws iam simulate-custom-policy for a draft, simulate-principal-policy for an existing user or role). Pass condition keys with --context-entries to test the office IP, a home IP, or MFA true/false. It also accepts --permissions-boundary-policy-input-list and --resource-policy. Organization policies (SCPs, RCPs) are best tested in a sandbox account.
  • IAM Access Analyzer policy validation (aws accessanalyzer validate-policy): more than 100 checks for errors (an invalid ARN like arn:awn), security warnings (iam:PassRole with *, Allow with NotAction), and suggestions, as you write.
  • Access Analyzer custom policy checks: check-no-new-access (does a new version grant anything a reference policy does not?), check-access-not-granted (does it allow iam:CreateUser or s3:DeleteBucket?), check-no-public-access (does a bucket or key policy make the resource public?). Put them in the CI pipeline so a risky change fails the build. Access Analyzer’s other features (external and unused access) are in lesson 2.4.

#What goes wrong

SymptomCauseFix
App on EC2 denied by an office-IP guardRequest came through a VPC endpoint; aws:SourceIp missing, NotIpAddress trueAdd aws:SourceVpce / aws:SourceVpc to the Deny
CloudFormation or Athena fails under an IP guardThe service calls with its own addressAdd "Bool": {"aws:ViaAWSService": "false"}
CLI users ignore the MFA ruleDeny + Bool false; key missing for access keysUse BoolIfExists
A tag restriction allows untagged requestsForAllValues on a missing key is trueAdd "Null": {"aws:TagKeys": "false"}
ListBucket → AccessDenied with s3:*Resource is bucket/* onlyAdd the bucket ARN
Cross-account call denied though the bucket policy allows itCaller’s identity policy is silentAllow the bucket ARN in the caller’s account too
Assumed role cannot write back to its own accountA role session has only the role’s permissionsLet the home bucket’s policy trust the role, or use a resource-based policy instead
Developer cannot create a roleMissing or wrong --permissions-boundaryPass the approved boundary ARN
An administrator still cannot do somethingBoundary, SCP or session policyCheck every limit, not just the identity policy
Queue receives messages from a stranger’s SNS topicService principal allowed without aws:SourceArnAdd aws:SourceArn / aws:SourceAccount

#How it is used in system design and on the exam

  • Network perimeter: IP and VPC endpoint conditions on sensitive buckets, written as Deny guards that understand VPC endpoints and service calls.
  • Identity perimeter: aws:PrincipalOrgID on resource policies and an RCP that rejects outsiders; resource perimeter: aws:ResourceOrgID in an SCP so insiders cannot copy data to outside buckets. Together they are AWS’s “data perimeter” pattern.
  • Scale with ABAC: tag-based policies for many teams and projects, with tag changes locked down.
  • Safe delegation: boundaries plus iam:PermissionsBoundary and narrow iam:PassRole, so teams move fast without a ticket queue.
  • Least privilege per job: session policies on a shared role.

#Try it yourself

With the CLI configured (lesson 1.3), try these against a test account. The simulator commands change nothing.

# 1. Test the office guard with different source addresses (nothing is attached)
aws iam simulate-custom-policy \
  --policy-input-list file://office-guard.json \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::payroll-example/jan.csv \
  --context-entries ContextKeyName=aws:SourceIp,ContextKeyValues=198.51.100.7,ContextKeyType=ip \
                    ContextKeyName=aws:ViaAWSService,ContextKeyValues=false,ContextKeyType=boolean \
  --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text
# s3:GetObject    explicitDeny
# Repeat with 203.0.113.25: allowed (if the file also contains an Allow statement)

# 2. Bool vs BoolIfExists: leave the MFA key out to imitate a long-term access key
aws iam simulate-custom-policy \
  --policy-input-list file://mfa-guard.json \
  --action-names s3:DeleteBucket --resource-arns arn:aws:s3:::reports-example \
  --context-entries ContextKeyName=aws:MultiFactorAuthPresent,ContextKeyValues=false,ContextKeyType=boolean \
  --query 'EvaluationResults[0].EvalDecision' --output text
# explicitDeny   (now delete the --context-entries line: Bool lets it through, BoolIfExists does not)

# 3. What can an existing role really do, boundary included?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/app/orders-fn-role \
  --action-names s3:GetObject iam:CreateUser \
  --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text
# s3:GetObject    allowed
# iam:CreateUser  implicitDeny

# 4. Create a role with the approved boundary, then put a boundary on an existing user
aws iam create-role --role-name app/orders-fn-role \
  --assume-role-policy-document file://lambda-trust.json \
  --permissions-boundary arn:aws:iam::111122223333:policy/DevBoundary
aws iam put-user-permissions-boundary --user-name tanvir \
  --permissions-boundary arn:aws:iam::111122223333:policy/DevBoundary
aws iam put-role-permissions-boundary --role-name legacy-etl \
  --permissions-boundary arn:aws:iam::111122223333:policy/DevBoundary

# 5. Validate a draft policy (finds the arn:awn typo and risky patterns)
aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY \
  --policy-document file://s3-policy.json \
  --query 'findings[].[findingType,issueCode]' --output text
# one ERROR line for the invalid arn:awn prefix, then any SECURITY_WARNING,
# WARNING and SUGGESTION findings (for example a PassRole with a wildcard)

# 6. A session policy: assume a broad role, receive narrow credentials
aws sts assume-role \
  --role-arn arn:aws:iam::111122223333:role/ExportRole \
  --role-session-name job-4711 \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::exports-example/customer-42/*"}]}'

# 7. Open a bucket to the whole organization
aws s3api put-bucket-policy --bucket finance-data-example --policy file://org-read.json

Then build the delegation pattern in a sandbox account: create DevBoundary, give a test user the developer policy above, and try aws iam create-role with and without --permissions-boundary, and then aws iam delete-role-permissions-boundary.

#Summary

Key points

  • A condition is operator → key → values. Operators and keys combine with AND, values for one key with OR; “A or B” across keys needs two statements.
  • A missing key makes a normal operator false, a negated operator true, and …IfExists true. That single rule explains the aws:SourceIp surprise (VPC endpoints, services on your behalf) and the Bool versus BoolIfExists MFA trap.
  • aws:SourceIp checks API requests from public addresses; use aws:SourceVpce/aws:SourceVpc for endpoint traffic and aws:ViaAWSService for service calls. It does not guard the console sign-in.
  • Multi-valued keys (aws:TagKeys, aws:CalledVia, aws:PrincipalOrgPaths) need ForAnyValue/ForAllValues; ForAllValues on a missing key is true, so add a Null check.
  • ABAC compares aws:PrincipalTag with aws:ResourceTag, forces tags with aws:RequestTag and must protect tag changes.
  • S3: ListBucket on the bucket ARN, object actions on bucket/*. The slide’s arn:awn is a typo for arn:aws.
  • aws:PrincipalOrgID opens a resource to the whole organization; aws:SourceArn/aws:SourceAccount stop the confused deputy for service principals.
  • Assuming a role gives up your own permissions; a resource-based policy keeps them. Across accounts both the caller’s identity policy and the resource policy must allow.
  • A permissions boundary (users and roles, never groups) is a ceiling. Delegation = allow CreateRole only with iam:PermissionsBoundary = the approved boundary, deny removing or editing it, narrow iam:PassRole.
  • Full order: Deny → RCP → SCP → resource-based → identity-based → boundary → session. Across accounts the flow runs once per account and both must allow.

#Self-check