What IAM Sees That You Don't

What IAM Sees That You Don’t

Dan Gansel September 25, 2026

Every IAM policy you write depends on condition keys – they’re the precision layer that turns “can call S3” into “can call S3 only from our VPC, using our identity, on resources we own.” They’re the backbone of least-privilege, data perimeters, and SCP guardrails.

But here’s the thing: for every request, the IAM engine assembles a request context – a set of key-value pairs – and your Condition block is matched against it. AWS documents individual keys, but the context as a whole isn’t surfaced. What does the engine actually assemble for a given request? How is it structured? What else is in there beyond the documented keys? We wanted to answer those questions.

We decided to find out. We built a technique that uses the IAM engine as its own oracle, tested roughly 36 million candidate key names across more than 150 AWS services, and found 36 undocumented condition keys.

The patterns are consistent across many services – too consistent to be accidental. Some of these keys expose live platform state that nothing in the public documentation can touch. And some of them, AWS already depends on in their own managed policies.

How We Found Them

The known technique

The idea of using IAM as a side channel isn’t new. Researchers like Ben Bridts and others showed that you can extract the value of a documented condition key – like s3:ResourceAccount – by attaching a session policy with a StringLike condition and observing whether the API call succeeds or is denied. Denied means the value matched; allowed means it didn’t. One binary signal per guess.

image-26-1024x991

Repeat that with prefix probing (1*, 10*, 11*, 12*…) and you can enumerate the value digit by digit – roughly 30 probes to extract a 12-digit account ID.

image-27-1024x494

This technique works because of a property of session policies: they take effect instantly via sts:AssumeRole, they intersect with the role’s identity policy, and the only variable is the condition you’re testing. No IAM propagation delay, no resource modification, trivially parallelizable.

But the existing technique has a limitation: it only works with condition keys you already know about. You pick a documented key like s3:ResourceAccount, and you extract its value. You can’t discover new keys this way – you can only read values from keys whose names are already public.

We found two primitives that turn value extraction into key discovery.

Primitive 1: IAM accepts any condition key name

IAM’s policy editor accepts any condition key – even nonsense like aws:ABCDTESTSourceVpc. It won’t reject it – it is an intentional design behavior for forward compatibility. The engine itself is the only authority that distinguishes real keys from fake ones.

image-23

This means you can put an arbitrary candidate key name in a session policy, and the engine will evaluate it. If the key exists in the request context, the condition fires. If it doesn’t exist, the condition is silently ignored.

Primitive 2: The Null operator checks key existence

IAM’s Null condition operator checks whether a condition key is present in the request context – “false” means “deny if this key exists and has a value,” “true” means “deny if this key is absent.”

image-24

Combining them: the oracle

Put these together and you get a key discovery oracle. The probe is a session policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": "*", "Resource": "*"},
    {
      "Effect": "Deny", "Action": "*", "Resource": "*",
      "Condition": {"Null": {"<candidate-key>": "false"}}
    }
  ]
}

The Null check asks: “is this key present in the request context?” If the subsequent API call is denied, the key exists. If it’s allowed, it doesn’t. One binary signal per candidate key name – not per value guess, but per key name. That’s the difference: the known technique extracts values from known keys; this discovers keys nobody knew existed.

Every run is calibrated: a known-present key (aws:PrincipalArn) must deny, a known-absent key (aws:TotallyFakeKey999) must allow.

Phase 1: Guessing names (22 keys)

We turned that single probe into a search. Phase 1 was brute force and creativity:

  • Pattern generation from botocore models – cross-referencing API parameter names, documented key patterns, and service-specific naming conventions. Found aws:PrincipalId, aws:ResourceArn, aws:ResourceRegion.
  • AI-invented candidates – domain knowledge about IAM internals, authentication flows, and AWS infrastructure, generated as candidate key names. Found aws:TokenAge, aws:action, aws:resource, aws:service, s3:BucketName, s3:CallerAccount.
  • Exhaustive brute-force – every possible string from 1 to 5 characters in the aws:* namespace, and 1-3 characters in s3:*. Over 12 million candidates. Found aws:arn, aws:type, aws:name, aws:namespace – ultra-short key names hiding in plain sight that no pattern generator would try.
  • Multi-service scanning – 45 AWS services via list operations. Found dynamodb:TableName, sqs:QueueName, sns:TopicName, events:RuleName, redshift:ClusterName, ecr:RepositoryName.

At peak throughput, the brute-force rig ran 5 AWS regions x 80 workers = 400 concurrent threads, processing ~540,000 keys per minute using direct HTTP signing instead of the boto3 SDK.

Phase 1 found 22 keys, then hit hard diminishing returns. Larger AI batches (10K candidates) and mass-generated lists (727K candidates from deep botocore extraction) returned nothing new.

Phase 2: Reading AWS’s own artifacts (14 more keys)

Phase 2 stopped guessing and started reading.

Managed policy mining: We extracted every condition key from the Condition blocks of all 1,577 AWS managed IAM policies and cross-referenced against four documentation sources. Two keys appeared in managed policies but in zero public documentation:

  • aws:PrincipalOrgMasterAccountId – used in AWSSSOServiceRolePolicy (the service-linked role for IAM Identity Center)
  • lakeformation:GlueARN – used in Lake Formation managed policies

CloudFormation schema analysis: We parsed primaryIdentifier and readOnlyProperties from 1,598 CloudFormation resource type schemas, converting them to candidate condition key names. The schemas are a structured, machine-readable source of canonical property names that AWS maintains. This found kms:KeyId, ecs:ClusterName, route53:HostedZoneId, apigateway:Resourcepath, codedeploy:ApplicationName, es:DomainName.

Resource-specific operation testing: Phase 1’s multi-service scan used list operations (ListBuckets, DescribeInstances). Phase 2 discovered that many keys only populate on resource-targeting operations – kms:DescribeKey populates kms:KeyId but kms:ListKeys doesn’t. Broader resource-specific testing found cloudtrail:TrailName, elasticfilesystem:FileSystemId, acm:CertificateId, ssm:DocumentName.

Hypothesis-driven testing: After finding aws:PrincipalOrgMasterAccountId, we hypothesized a resource-side counterpart following the Principal*/Resource* duality seen in documented keys. Tested 27 variations – only aws:ResourceOrgMasterAccountId was present.

What we didn’t find

We tested thousands of candidates for keys that security practitioners would love to have – Lambda runtime, Bedrock inference parameters, TLS data, estimated cost, source IP country, and many more. None of them exist in the request context for the API calls we tested.

That said, a key being absent on one operation doesn’t mean it’s absent everywhere. The request context changes depending on which API call you make and how the service implements its IAM integration. We covered a lot of ground – but the search space is enormous, and every service has its own quirks. Some of these keys may well exist on operations or in contexts we didn’t try.

What We Found

36 undocumented condition keys – 13 global, 23 service-specific. The count matters less than what they reveal about how the engine actually works. They sort into three groups, and together they paint a new picture of the request model.

Group 1: The request decomposition

This was the biggest surprise. When you write "Action": "s3:GetObject" in a policy, you think of it as an atomic string. The engine doesn’t. It breaks it apart:

Action decomposition:

KeyExample (S3 GetObject)What it holds
aws:actions3:GetObjectFull IAM action (service:operation)
aws:services3Service namespace only
aws:nameGetObjectOperation name only

Resource decomposition:

KeyExample (S3 GetObject)Example (EC2 DescribeInstanceAttribute)
aws:ResourceArnarn:aws:s3:::bucket/keyarn:aws:ec2:region:account:instance/i-xxx
aws:resourcebucket/keyinstance/i-xxx
aws:typeobjectinstance

Principal and metadata:

KeyValue
aws:PrincipalId{RoleId}:{SessionName}
aws:arnContext-dependent (see anomalies below)
aws:namespaceAccount ID (not service namespace)
aws:ResourceRegionResource’s AWS region
aws:TokenAgeSeconds since token issuance (numeric)
aws:PrincipalOrgMasterAccountIdOrganization management account ID
aws:ResourceOrgMasterAccountIdResource owner’s management account ID

Every Action you write in policy is a flattened projection of this three-part decomposition. Every Resource ARN is broken into its path and type. The engine has always worked on the decomposed form – you’ve just never been able to condition on it.

That opens up policy patterns that are impossible today. aws:name lets you match on the operation name across all services – a single condition that catches every Delete* or Create* call, regardless of which service it belongs to. We’ll cover the policy use cases in a follow-up post.

Group 2: The {service}:{ResourceName} pattern

The largest group, and the one that made us realize this isn’t a handful of leftover internals – it’s a systemic design pattern. Almost every key follows the same naming convention: {service}:{PrimaryResourceName}.

KeyServiceValue
s3:BucketNameS3Bucket name
lambda:FunctionNameLambdaFunction name
iam:RoleNameIAMRole name
dynamodb:TableNameDynamoDBTable name
sqs:QueueNameSQSQueue name
sns:TopicNameSNSTopic name
events:RuleNameEventBridgeRule name
redshift:ClusterNameRedshiftCluster name
ecr:RepositoryNameECRRepository name
kms:KeyIdKMSKey UUID
ecs:ClusterNameECSCluster name
route53:HostedZoneIdRoute 53Hosted zone ID
codedeploy:ApplicationNameCodeDeployApplication name
es:DomainNameElasticsearchDomain name
cloudtrail:TrailNameCloudTrailTrail name
elasticfilesystem:FileSystemIdEFSFile system ID
acm:CertificateIdACMCertificate UUID
ssm:DocumentNameSSMDocument name

Nineteen services, one convention. Once we spotted the pattern, it became predictive – CloudFormation schema analysis correctly guessed 5 of 11 previously-found service keys and surfaced 6 new ones. That predictive power is the tell: this is infrastructure, not accidents. The IAM engine knows the primary resource name for almost every service, and it puts it in the request context every time.

Group 3: The ones that don’t fit

Then there are the keys that break every pattern we just described. These are the edge cases that prove the request context isn’t a clean abstraction – it’s a living system with quirks.

s3:CurrentPolicyStatus – reports whether a bucket’s policy currently evaluates as public or nonpublic. This is live policy state exposed as a condition key. The value changes dynamically and immediately when you modify a bucket policy.

apigateway:Resourcepath – exposes an internal REST API routing path (/restapis/{id}, /account), not a resource name. A different pattern entirely – request-path-based rather than resource-identifier-based.

s3:CallerAccount – the caller’s account ID. Redundant with aws:PrincipalAccount (documented). Its existence suggests services may populate their own copies of common context values.

lakeformation:GlueARN – only surfaces during Lake Formation credential vending. AWS’s own managed policy gates on it.

s3:Objectpath – confirmed present on s3:GetObject.

The three-tier visibility hierarchy

Not all undocumented keys are treated equally in terms of visibility. We discovered three tiers:

TierDescriptionCountExample
1: DocumentedIn public docs, passes ValidatePolicy–aws:PrincipalArn, s3:authType
2: Recognized but undocumentedPasses ValidatePolicy, absent from all docs1aws:PrincipalOrgMasterAccountId
3: Undocumented but evaluatedFails ValidatePolicy, absent from all docs, but IS populated35Everything else

We confirmed this by brute-forcing 8,059 candidates against Access Analyzer’s ValidatePolicy API. Only aws:PrincipalOrgMasterAccountId occupies Tier 2 – the single key AWS recognizes in their tooling but hasn’t documented. Every other undocumented key fails ValidatePolicy entirely.

The tier distinction matters because it means IAM Access Analyzer, Policy Simulator, and every third-party policy linting tool will flag any policy using these keys as invalid – even though the engine evaluates them.

The rough edges

The patterns above are real, but they aren’t perfectly uniform. The same key can behave differently across services, some keys aren’t populated where you’d expect them, and naming doesn’t always mean what you think. The details matter if you’re writing policies against these – a condition that works on one service might silently do nothing on another.

We documented the specific inconsistencies we found across services in detail. The short version: test any key against the specific operation you care about before relying on it in policy. The patterns are strong enough to guide discovery, but not strong enough to assume uniform behavior.

The bigger picture

The individual keys are useful – some enable genuinely new policy capabilities that the documented set cannot express today. We’ll cover those in detail in a follow-up post.

But the real discovery is the engine’s internal architecture. IAM decomposes every request into a structured context with action, resource, and principal dimensions – each broken into constituent parts. These decomposed values are the engine’s native vocabulary. Documented condition keys are the subset AWS chose to expose; these 36 are part of the rest.

Understanding the model changes how you read every policy you own. The Action and Resource elements aren’t the whole story – they’re a flattened view of a richer decomposition that’s always been there, evaluated on every request, invisible to everyone who doesn’t probe for it.

And now that the technique is out there, anyone can do this. The oracle is a session policy and a Null check – no special tooling, no privileged access, nothing beyond what IAM gives you for free. Every AWS customer can probe their own request context and discover what the engine actually sees. We found 36 keys. There are almost certainly more, on services and operations we didn’t cover. The search is open.

Until then, this is what we have. Looking forward to hearing about new condition keys found using this technique!

Contents

Further Reading

Let Me Speak to Your Manager (Account)

Let Me Speak to Your Manager (Account)

The management account is the most privileged account in any AWS Organization. It controls SCPs, creates and deletes member accounts, manages IAM Identity Center, and is itself exempt from SCPs. Getting its 12-digit account ID is the first step in targeting it. The documented way to get it is organizations:DescribeOrganization - but security-conscious environments restrict…
Configuration-Focus

Introducing the new Configurations experience in Upwind

Compliance should not be a fire drill! Ask a security team how audit season goes and you will often hear a version of the same story. Someone pulls a list of cloud accounts. Someone else exports findings into a spreadsheet that is already outdated by the time it is shared. Screenshots get pasted into a…
What You Could Build If IAM Let You

What You Could Build If IAM Let You: New Policies From Undocumented Condition Keys

In the previous post, we mapped 36 condition keys that the IAM engine evaluates but has never documented. The decomposition model, the service-specific resource identifiers, the organizational metadata - all of it sitting in the request context, invisible unless you probe for it. That post was about discovery. This one is about what you can…
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Threat RSS
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Main RSS