Initializing Enclave...

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 + NotAction with Resource: "*", meaning the principal can perform every AWS action in existence except the tiny exclusion list — including iam:CreateUser, sts:AssumeRole, and s3:*.
  • How to fix it: Replace NotAction + Allow with an explicit Action allowlist scoped to the minimum required permissions, and lock Resource to 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:

  1. Enumerate: iam:ListRoles, iam:ListPolicies — both allowed, not in the exclusion list.
  2. Escalate: iam:AttachRolePolicy, iam:CreateAccessKey, iam:PassRole — all allowed.
  3. Persist: Create a new IAM user or OIDC identity provider. Attach AdministratorAccess.
  4. Exfiltrate: s3:GetObject across every bucket, secretsmanager:GetSecretValue, ssm:GetParameter — unrestricted.
  5. 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 NotAction instead of an explicit Action list.
  • 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 Action list — you own the allowlist, not the exclusion list.
  • Scoped Resource ARNs — no wildcard * on resource.
  • Condition keys — region lock + TLS enforcement.
  • Explicit Deny for 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 NotAction patterns using boto3 + 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.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →