Diagnosing a Hidden AWS IAM Policy Design Configuration Trap in Amazon Web Services
A deceptive AWS IAM NotResource-plus-Allow statement grants access to every bucket except the one used for testing. Diagnose, correct and roll back safely.
Operational summary
At a glance
- Verification signal
- Re-run the detection test and a controlled negative-path test.
- Rollback or recovery
- Restore the exported configuration if the new control blocks required production traffic, then narrow the policy before redeployment.
Symptom
During a policy review conducted in an isolated, non-production AWS account, a platform engineering team tested an IAM role that was designed to restrict an application to reading objects from a single Amazon S3 bucket. Manual verification against the intended bucket behaved exactly as expected: requests to the named bucket succeeded, and the team recorded the change as validated. A later access review found the same role could also list and read objects in every other bucket in the account, none of which the workload was supposed to reach.
False Assumption
The engineer who wrote the policy assumed that naming a specific bucket ARN inside a statement, on its own, is enough to scope an Allow effect to that one bucket. The statement had been copied from an internal template using the NotResource element, on the understanding that “NotResource with one bucket named” reads as “grant access to only this bucket.” In AWS IAM’s policy grammar, NotResource paired with Effect: Allow does the opposite: it grants the listed actions on every resource except the ones named. Treating Resource and NotResource as interchangeable ways of writing “just this one resource” is the misleading step that produced the trap.
Root Cause
The deployed statement read:
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"NotResource": [
"arn:aws:s3:::intended-bucket",
"arn:aws:s3:::intended-bucket/*"
]
}
Because the statement excludes intended-bucket from its scope, no access is granted to that bucket by this statement; access to it depends entirely on another policy. For every other bucket in the account, the statement grants both actions unconditionally. The manual check against intended-bucket only exercised the one resource the statement explicitly excludes, so the test could not reveal the excess grant elsewhere. The trap is deceptive precisely because the resource used for validation is the one resource the policy does not touch.
Diagnosis
Confirming the real effect of a statement requires testing resources outside the one named in it, not only the one inside it.
- Command:
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names s3:GetObject s3:ListBucket --resource-arns arn:aws:s3:::unrelated-bucket/object-key— read-only. Confirms whether effective permissions extend beyond the intended bucket. - Command:
aws iam get-policy-version --policy-arn <policy-arn> --version-id <current-version>— read-only. Retrieves the exact deployed statement, since console summaries can compressResourceandNotResourceinto similar-looking labels.
Expected evidence of the trap: the simulation for an unrelated bucket returns allowed for both actions, even though the change record states access should be limited to one bucket.
Correction
The statement must name the intended bucket directly under Resource, not under NotResource:
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::intended-bucket",
"arn:aws:s3:::intended-bucket/*"
]
}
Publish the corrected statement as a new policy version rather than editing in place, so the previous version remains available for rollback:
- Command:
aws iam create-policy-version --policy-arn <policy-arn> --policy-document file://corrected-policy.json --set-as-default— state-changing. Publishes the corrected statement while preserving the prior version.
Apply this only in the isolated validation account first, and confirm the role has no other attached policy separately granting the same broad access before treating the correction as complete.
Validation
- Simulate
s3:GetObjectands3:ListBucketagainstintended-bucket; expected result:allowed. - Simulate the same actions against the unrelated bucket used during diagnosis; expected result:
implicitDenyorexplicitDeny, with no other attached policy providing an alternative allow. - List all policies attached to the role with
aws iam list-attached-role-policiesandaws iam list-role-policies; confirm no remaining statement grants the excluded actions outsideintended-bucket.
Rollback
If the corrected policy blocks a legitimate access path that had unintentionally depended on the wider grant, do not re-introduce NotResource. Instead:
- Identify the prior version ID with
aws iam list-policy-versions --policy-arn <policy-arn>— read-only. - Restore it only as a temporary measure with
aws iam set-default-policy-version --policy-arn <policy-arn> --version-id <previous-version-id>— state-changing. - Record the legitimate access path that depended on the wider grant, and write a deliberate, explicitly scoped statement for it rather than leaving the reverted broad grant in place.
Keep the reverted version in place only long enough to design the scoped replacement; treat it as a stop-gap, not a resolution.
Prevention
The underlying design lesson matches AWS’s own published guidance on protecting workloads through deliberate, least-privilege permission design. Three checks reduce the chance of this trap recurring:
- Treat any statement combining
NotResourceorNotActionwithEffect: Allowas requiring a second reviewer, since the exclusion logic runs opposite to how a scoping element instinctively reads. - Require that every policy validation pass includes at least one resource outside the intended scope, not only the intended resource.
- Where a policy linter or access-analysis tool is available, run it against every new or modified statement before it reaches a shared account, and treat any flagged
NotResource/NotActioncombination withAllowas needing explicit sign-off rather than routine approval.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
AllowNotResourceEffect: AllowVerify, roll back or escalate
Verify
Re-run the detection test and a controlled negative-path test. Confirm the unsafe behaviour is blocked while approved traffic still succeeds.
Rollback
Restore the exported configuration if the new control blocks required production traffic, then narrow the policy before redeployment.
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.