Initializing Enclave...

How to Fix IAM 'Not Authorized to Perform ec2:DescribeInstances' Even With AdministratorAccess

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins


TL;DR

  • What broke: An explicit Deny in an SCP, permission boundary, or inline policy is overriding the AdministratorAccess managed policy. In AWS IAM, explicit Deny always wins — AdministratorAccess is not a trump card.
  • How to fix it: Locate the conflicting explicit Deny using aws iam simulate-principal-policy and either remove it, scope it correctly with conditions, or adjust the permission boundary to allow ec2:DescribeInstances.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your policy JSON and let it pinpoint the conflicting statement without sending your ARNs to a third-party server.

The Incident (What Does the Error Mean?)

Raw error output:

An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation:
You are not authorized to perform this operation.

// Or via CLI:
An error occurred (AccessDenied) when calling the DescribeInstances operation:
User: arn:aws:iam::123456789012:user/dev-user is not authorized to perform:
ec2:DescribeInstances because no identity-based policy allows the ec2:DescribeInstances action

Immediate consequence: The IAM principal cannot enumerate EC2 instances. Any automation, deployment pipeline, monitoring agent, or developer workflow depending on instance discovery is dead in the water. If this is a CI/CD role, your entire deployment is blocked.

The error message itself is misleading. It says "no identity-based policy allows" the action — but the real culprit is almost always an explicit Deny upstream that the evaluation engine hits before it ever reaches your AdministratorAccess allow.


The Attack Vector / Blast Radius

This misconfiguration has a dual failure mode:

Operational blast radius: ec2:DescribeInstances is a foundational read-only API call. The AWS Console EC2 dashboard, Terraform state refresh, Ansible dynamic inventory, AWS Systems Manager, and virtually every third-party monitoring tool (Datadog, Grafana, etc.) depend on it. A broken permission boundary silently breaks all of these simultaneously.

Security blast radius: The inverse problem is equally dangerous. If you're using SCPs or permission boundaries to restrict AdministratorAccess and they're misconfigured, you may have gaps — actions you intended to deny that are actually allowed. A threat actor with access to this principal can exploit those gaps. Misconfigured SCPs that are too broad (e.g., denying ec2:* when you meant to deny ec2:TerminateInstances) are a common source of both outages and security holes.

AWS IAM Policy Evaluation Order (the hierarchy that bites you):

  1. Explicit Deny in any policy → STOP. Denied.
  2. SCP Allow
  3. Resource-based policy Allow
  4. Permission Boundary Allow
  5. Identity-based policy Allow (this is where AdministratorAccess lives)
  6. Session policy Allow

AdministratorAccess only operates at step 5. An explicit Deny at step 1 from an SCP at the AWS Organization level cannot be overridden by any identity-based policy, period.


How to Fix It

Step 0: Diagnose First

Run the policy simulator before touching anything:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:user/dev-user \
  --action-names ec2:DescribeInstances \
  --resource-arns "*"

The output will show DENY with MatchedStatements identifying the exact policy and statement ID causing the block. Read this output before proceeding.


Basic Fix: Remove or Correct the Explicit Deny

If the denial is in an inline policy or managed policy attached directly to the user:

{
  "Version": "2012-10-17",
  "Statement": [
-   {
-     "Sid": "DenyEC2",
-     "Effect": "Deny",
-     "Action": "ec2:*",
-     "Resource": "*"
-   }
+   {
+     "Sid": "DenyDestructiveEC2Only",
+     "Effect": "Deny",
+     "Action": [
+       "ec2:TerminateInstances",
+       "ec2:DeleteVolume",
+       "ec2:DeleteSnapshot"
+     ],
+     "Resource": "*"
+   }
  ]
}

Fix: SCP at AWS Organizations Level

If simulate-principal-policy points to an SCP, you must fix it in AWS Organizations → Policies, not in IAM:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RestrictEC2Actions",
      "Effect": "Deny",
-     "Action": "ec2:*",
+     "Action": [
+       "ec2:TerminateInstances",
+       "ec2:ModifyInstanceAttribute"
+     ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    }
  ]
}

Fix: Permission Boundary

If a permission boundary is set on the user/role and doesn't explicitly allow ec2:DescribeInstances, the action is denied even if AdministratorAccess allows it:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowedServices",
      "Effect": "Allow",
      "Action": [
        "s3:*",
-       "ec2:RunInstances"
+       "ec2:RunInstances",
+       "ec2:DescribeInstances",
+       "ec2:DescribeInstanceStatus",
+       "ec2:DescribeTags"
      ],
      "Resource": "*"
    }
  ]
}

Permission boundaries are whitelists, not blacklists. If ec2:DescribeInstances is not explicitly listed in the boundary, it's denied regardless of what AdministratorAccess says.


Enterprise Best Practice: Least-Privilege with Condition Keys

Don't grant blanket EC2 read. Scope describe permissions with resource tags and region conditions:

{
  "Version": "2012-10-17",
  "Statement": [
-   {
-     "Effect": "Allow",
-     "Action": "ec2:*",
-     "Resource": "*"
-   }
+   {
+     "Sid": "EC2ReadScopedByRegionAndTag",
+     "Effect": "Allow",
+     "Action": [
+       "ec2:DescribeInstances",
+       "ec2:DescribeInstanceStatus",
+       "ec2:DescribeTags",
+       "ec2:DescribeVolumes"
+     ],
+     "Resource": "*",
+     "Condition": {
+       "StringEquals": {
+         "aws:RequestedRegion": "us-east-1",
+         "aws:ResourceTag/Environment": "production"
+       }
+     }
+   }
  ]
}

💡 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

1. Checkov — scan IaC before it ships:

checkov -f iam_policy.json --check CKV_AWS_40,CKV_AWS_274
# CKV_AWS_274: Ensure IAM policy does not have statements with admin permissions
# CKV_AWS_40: Ensure IAM policies are attached only to groups or roles

2. OPA/Conftest policy gate for Terraform plans:

# deny_wildcard_ec2_deny.rego
package terraform.iam

deny[msg] {
  stmt := input.resource.aws_iam_policy.policy.statement[_]
  stmt.effect == "Deny"
  stmt.actions[_] == "ec2:*"
  msg := "Blanket ec2:* Deny detected. Scope to specific destructive actions only."
}

3. AWS Config Rule — continuous compliance:

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "iam-no-wildcard-ec2-deny",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "IAM_POLICY_NO_STATEMENTS_WITH_ADMIN_ACCESS"
  }
}'

4. Enforce permission boundaries at account level via SCP:

Require that all IAM roles created by developers have a permission boundary attached, preventing privilege escalation and ensuring the boundary is always the final arbiter:

{
  "Sid": "RequirePermissionBoundary",
  "Effect": "Deny",
  "Action": ["iam:CreateRole", "iam:PutRolePolicy"],
  "Resource": "*",
  "Condition": {
    "StringNotLike": {
      "iam:PermissionsBoundary": "arn:aws:iam::*:policy/StandardDevBoundary"
    }
  }
}

This ensures no developer-created role can ever operate without the boundary — and the boundary becomes your single point of control for what actions are permissible.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →