Home / AWS

AWSCheat sheetAdvanced

AWS IAM global condition keys cheat sheet: data perimeters, confused-deputy protection and the missing-key traps

The global condition keys that matter for org, network and service perimeters, when each key is present or missing, operator traps like ForAllValues and IfExists, and how to test conditions before rolling them out.

Deepak Bhardwaj7 min read

Applies toAWS IAM identity and resource policies, SCPs and RCPs, role trust policies, IAM policy simulator, IAM Access Analyzer policy validation

Most broken guardrails don't fail because someone used the wrong key. They fail because the key wasn't in the request context, and the operator treated "missing" in a way nobody expected. A deny that should block outsiders also blocks CloudTrail writing to your log bucket. An IP allow list stops working the day traffic moves to a VPC endpoint. This sheet lists the keys, when each one is present, and the patterns that hold up in production.

Missing-key behaviour: the rules to memorise

Condition form Key missing from the request Consequence
StringEquals, Bool, IpAddress, ... Evaluates false An Allow doesn't apply; a Deny doesn't apply
StringNotEquals, NotIpAddress, ArnNotLike, ... Evaluates true A Deny applies: this is what locks out service principals
...IfExists (e.g. StringNotEqualsIfExists) Evaluates true Use it when "absent" should count as "OK" for an Allow, or "skip" for a negated Deny
Null: {"key": "true"} True if the key is absent Use it to require a key explicitly
ForAllValues: True (empty set) Allows that rely on it pass when the key is missing; pair with Null: false
ForAnyValue: False Safe default for Deny statements on multivalued keys

Warning

Never write "ForAllValues:StringEquals": {"aws:TagKeys": [...]} in an Allow on its own. A request with no tags passes. Add "Null": {"aws:TagKeys": "false"} if tags are required.

Organisation and account boundary keys

Key Present when Typical use
aws:PrincipalOrgID Caller is an IAM principal in an account in an organisation Resource policy: only principals from my org
aws:PrincipalOrgPaths Same, multivalued OU paths Narrow to an OU: ForAnyValue:StringLike o-abc/r-xyz/ou-prod*
aws:PrincipalAccount Always for IAM principals Exact account checks
aws:ResourceOrgID, aws:ResourceAccount, aws:ResourceOrgPaths Resource owner is known to the service Identity/SCP side: only touch resources in my org
aws:SourceOrgID, aws:SourceAccount, aws:SourceArn A service principal acts on behalf of a resource Confused-deputy protection
aws:PrincipalIsAWSService Always: true for service principals such as cloudtrail.amazonaws.com Exempt service principals from org checks

Resource perimeter on a bucket, with a service-principal exemption so log delivery keeps working:

{
  "Sid": "DenyOutsideOrg",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": ["arn:aws:s3:::corp-logs", "arn:aws:s3:::corp-logs/*"],
  "Condition": {
    "StringNotEqualsIfExists": { "aws:PrincipalOrgID": "o-a1b2c3d4e5" },
    "BoolIfExists": { "aws:PrincipalIsAWSService": "false" }
  }
}

Both conditions must be true for the deny to apply: the caller isn't in the org and isn't an AWS service principal. Service principals are then held in check by aws:SourceOrgID or aws:SourceAccount in the Allow that grants them access.

Confused deputy: trust and resource policies for service principals

Any policy that grants a service principal access should pin which resource it acts for:

{
  "Effect": "Allow",
  "Principal": { "Service": "sns.amazonaws.com" },
  "Action": "sqs:SendMessage",
  "Resource": "arn:aws:sqs:eu-west-1:111122223333:orders",
  "Condition": {
    "ArnEquals": { "aws:SourceArn": "arn:aws:sns:eu-west-1:111122223333:orders-topic" }
  }
}

Use aws:SourceArn when you know the exact resource. Use aws:SourceAccount or aws:SourceOrgID when the resource ARN isn't known in advance or varies. Not every service sets these keys for every integration; check the service's documentation before relying on them.

Network perimeter keys

Key Present when Trap
aws:SourceIp Request arrives over the public endpoint Absent when the request comes through a VPC endpoint. For calls an AWS service makes on your behalf, it holds the service's address, not yours
aws:SourceVpc, aws:SourceVpce Request arrives through a VPC endpoint Absent on public endpoint traffic
aws:VpcSourceIp Through a VPC endpoint; the client's private IP Private ranges overlap across VPCs; combine with aws:SourceVpc
aws:ViaAWSService True when a service (e.g. CloudFormation, Athena, KMS via S3) calls onward using your credentials Network conditions must exempt these calls or the service breaks
aws:EC2InstanceSourceVPC, aws:EC2InstanceSourcePrivateIPv4 Credentials came from an EC2 instance role Lets you pin instance credentials to their VPC even if they're exfiltrated

Deny requests from outside corporate IPs or your VPCs, without breaking AWS services or service-to-service calls:

{
  "Sid": "DenyOutsideNetwork",
  "Effect": "Deny",
  "Action": "*",
  "Resource": "*",
  "Condition": {
    "NotIpAddressIfExists": { "aws:SourceIp": ["203.0.113.0/24"] },
    "StringNotEqualsIfExists": { "aws:SourceVpc": ["vpc-0aaa111", "vpc-0bbb222"] },
    "BoolIfExists": {
      "aws:ViaAWSService": "false",
      "aws:PrincipalIsAWSService": "false"
    }
  }
}

Caution

Applied as an SCP or identity policy to humans and CI roles, a network deny like this can block console sessions from new office ranges or VPN egress addresses. It also blocks pipelines on hosted runners, whose IPs change. Roll it out with an exemption tag such as aws:PrincipalTag/perimeter-exempt, and test from every access path first.

Session and credential keys

Key Notes
aws:PrincipalArn For role sessions, the role ARN, not the session ARN. This is the key to use for "this role only"
aws:userid Role sessions: AROA...:session-name; useful for per-session matching
aws:SourceIdentity Set at assume-role time and persists through role chaining; good for tying actions to a human
aws:MultiFactorAuthPresent Absent for long-term access keys. Use BoolIfExists ... false in a Deny to catch both no-MFA cases
aws:MultiFactorAuthAge Seconds since MFA; absent without MFA
aws:TokenIssueTime Use to revoke sessions issued before a time (the console's "revoke active sessions" does this)
aws:PrincipalTag/<key> Tags on the role/user or session tags. The basis of ABAC
aws:RequestedRegion Global services (IAM, Organizations, Route 53, CloudFront, STS global endpoint) report us-east-1; exempt them with NotAction in Region-deny SCPs
aws:SecureTransport Deny false on S3 bucket policies to force TLS
aws:CalledVia, aws:CalledViaFirst, aws:CalledViaLast Only populated for a limited set of services that forward requests; check the list before relying on it

Require MFA for destructive IAM actions, covering access keys too:

{
  "Effect": "Deny",
  "Action": ["iam:DeleteRole", "iam:DeleteUser", "iam:PutRolePolicy", "iam:AttachRolePolicy"],
  "Resource": "*",
  "Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } }
}

Test conditions before rollout

Simulate a request with specific context values:

aws iam simulate-custom-policy \
  --policy-input-list file://perimeter.json \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::corp-logs/2026/11/13/file.gz \
  --context-entries \
    ContextKeyName=aws:SourceIp,ContextKeyValues=198.51.100.7,ContextKeyType=ip \
    ContextKeyName=aws:ViaAWSService,ContextKeyValues=false,ContextKeyType=boolean \
  --query 'EvaluationResults[].{action:EvalActionName,decision:EvalDecision}'

To see which context keys a policy references, and so which ones you must supply:

aws iam get-context-keys-for-custom-policy --policy-input-list file://perimeter.json

Lint with Access Analyzer. It catches unsupported keys for an action, operator/type mismatches, and ForAllValues used on single-valued keys:

aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY \
  --validated-resource-type AWS::S3::Bucket \
  --policy-document file://bucket-policy.json \
  --query 'findings[].{type:findingType,issue:issueCode,detail:findingDetails}' --output table

Then confirm in production with CloudTrail, which shows real denials and their errorMessage:

aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventSource,AttributeValue=s3.amazonaws.com \
  --max-results 50 --query 'Events[].CloudTrailEvent' --output text \
  | jq -r 'fromjson | select(.errorCode=="AccessDenied") | [.eventTime,.userIdentity.arn,.sourceIPAddress,.errorMessage] | @tsv'

S3 data events appear here only if your trail or event data store records them.

Prevent it next time

  • Keep perimeter statements in one reviewed module, such as an SCP/RCP library or a Terraform module, instead of copy-pasting them into bucket policies.
  • Add the service-principal and aws:ViaAWSService exemptions by default. Remove them only with a written reason.
  • Run validate-policy in CI on every policy change, and fail the build on ERROR and SECURITY_WARNING findings.
  • Before enforcing a network or org deny, deploy it with a non-matching test principal and review CloudTrail AccessDenied events for a week.