How to Fix IAM NotAction with Allow and No Resource Restriction in AWS Policies
Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 15 mins
TL;DR
- What broke: An IAM policy uses
Effect: Allow+NotActionwithResource: "*", meaning the principal can perform every AWS action in existence except the tiny exclusion list — includingiam:CreateUser,sts:AssumeRole, ands3:*. - How to fix it: Replace
NotAction+Allowwith an explicitActionallowlist scoped to the minimum required permissions, and lockResourceto specific ARNs. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your policy and get a least-privilege rewrite without leaking ARNs to a third-party server.
The Incident (What does the error mean?)
Your policy scanner or manual audit flagged a statement that looks like this:
{
"Effect": "Allow",
"NotAction": [
"iam:DeleteAccount",
"aws-portal:ModifyBilling"
],
"Resource": "*"
}
Immediate consequence: This statement does not allow two actions. It allows every other action across every AWS service on every resource in the account. The exclusion list gives a false sense of security. The principal attached to this policy is effectively an account administrator — without being in the AdministratorAccess managed policy, which at least shows up obviously in audits.
The Attack Vector / Blast Radius
NotAction with Allow is the IAM equivalent of a firewall rule that says "block port 22, allow everything else on 0.0.0.0/0." The blast radius is account-wide.
Privilege escalation path an attacker follows after compromising credentials bound to this policy:
- Enumerate:
iam:ListRoles,iam:ListPolicies— both allowed, not in the exclusion list. - Escalate:
iam:AttachRolePolicy,iam:CreateAccessKey,iam:PassRole— all allowed. - Persist: Create a new IAM user or OIDC identity provider. Attach
AdministratorAccess. - Exfiltrate:
s3:GetObjectacross every bucket,secretsmanager:GetSecretValue,ssm:GetParameter— unrestricted. - Destroy:
ec2:TerminateInstances,rds:DeleteDBInstance,lambda:DeleteFunction.
The two blocked actions (iam:DeleteAccount, aws-portal:ModifyBilling) are nearly irrelevant. The attacker already owns the account.
Common scenarios where this misconfiguration appears:
- A developer tried to "block billing changes" while giving a service role broad access — used
NotActioninstead of an explicitActionlist. - A Terraform module was copy-pasted from a Stack Overflow answer circa 2017.
- A break-glass policy was never scoped down after an incident.
How to Fix It (The Solution)
Basic Fix — Replace NotAction with an Explicit Action Allowlist
{
"Version": "2012-10-17",
"Statement": [
- {
- "Effect": "Allow",
- "NotAction": [
- "iam:DeleteAccount",
- "aws-portal:ModifyBilling"
- ],
- "Resource": "*"
- }
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::my-app-bucket",
+ "arn:aws:s3:::my-app-bucket/*"
+ ]
+ }
]
}
Stop there only if this is a low-risk internal service. For anything touching production data or IAM, continue to the enterprise pattern.
Enterprise Best Practice — Least Privilege with Permission Boundaries and Condition Keys
{
"Version": "2012-10-17",
"Statement": [
- {
- "Effect": "Allow",
- "NotAction": [
- "iam:DeleteAccount",
- "aws-portal:ModifyBilling"
- ],
- "Resource": "*"
- }
+ {
+ "Sid": "ScopedS3ReadWrite",
+ "Effect": "Allow",
+ "Action": [
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:DeleteObject",
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::my-app-bucket",
+ "arn:aws:s3:::my-app-bucket/*"
+ ],
+ "Condition": {
+ "StringEquals": {
+ "aws:RequestedRegion": "us-east-1"
+ },
+ "Bool": {
+ "aws:SecureTransport": "true"
+ }
+ }
+ },
+ {
+ "Sid": "DenyIAMMutation",
+ "Effect": "Deny",
+ "Action": [
+ "iam:*",
+ "sts:AssumeRole",
+ "organizations:*"
+ ],
+ "Resource": "*"
+ }
]
}
Key changes:
- Explicit
Actionlist — you own the allowlist, not the exclusion list. - Scoped
ResourceARNs — no wildcard*on resource. - Condition keys — region lock + TLS enforcement.
- Explicit
Denyfor IAM mutation — belt-and-suspenders; even if another policy grants IAM access, this Deny wins. - Attach a Permission Boundary to the role (
iam:PutRolePermissionsBoundary) so even if the policy is later modified, the boundary caps effective permissions.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of misconfiguration should never reach a pull request merge. Wire in the following:
1. Checkov — catches NotAction + Allow + Resource:* at plan time:
# .checkov.yml
check:
- CKV_AWS_40 # IAM policies should not have wildcard resource with Allow
- CKV2_AWS_40 # Detects NotAction misuse patterns
Run in CI: checkov -d ./iam --check CKV_AWS_40,CKV2_AWS_40 --hard-fail-on HIGH
2. OPA / Conftest policy for Terraform plan JSON:
package terraform.iam
deny[msg] {
stmt := input.resource_changes[_].change.after.policy_document[_].statement[_]
stmt.effect == "Allow"
stmt.not_actions != null
count(stmt.not_actions) > 0
stmt.resources[_] == "*"
msg := sprintf("CRITICAL: IAM statement uses NotAction+Allow+Resource:* in %v", [input.resource_changes[_].address])
}
3. AWS Config Rule — continuous detection in live accounts:
- Enable managed rule
iam-policy-no-statements-with-admin-access - Supplement with a custom Config rule invoking a Lambda that parses
NotActionpatterns usingboto3+json.loads(policy_document)
4. SCPs at the AWS Organizations level — hard guardrail:
{
"Effect": "Deny",
"Action": "iam:PutRolePolicy",
"Resource": "*",
"Condition": {
"StringLike": {
"iam:PolicyDocument": "*NotAction*"
}
}
}
This SCP blocks any principal from attaching a policy containing NotAction to a role — enforced at the organization layer, bypasses nothing.
5. Pre-commit hook using aws-iam-policy-validator:
pip install aws-iam-policy-validator
aws-iam-policy-validator validate \
--policy-type identity \
--policy file://policy.json \
--region us-east-1
Fails the commit if the policy contains overly permissive patterns before it ever hits a PR.