Initializing Enclave...

How to Fix AWS IAM AccessDenied on s3:GetObject Caused by Wildcard Policy Misconfiguration

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

TL;DR

  • What broke: An IAM policy with "Action": "*" or "*:*" is either being overridden by an explicit Deny in an SCP/permission boundary, or the wildcard is syntactically malformed — either way, s3:GetObject is denied and your application is dead in the water.
  • How to fix it: Replace the wildcard action block with an explicit, scoped least-privilege statement targeting only the required S3 actions and the specific bucket ARN.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your policy JSON and get a hardened replacement without sending your ARNs to a third-party server.

The Incident (What does the error mean?)

Raw error from CloudTrail or application logs:

An error occurred (AccessDenied) when calling the GetObject operation:
 User: arn:aws:iam::123456789012:role/my-app-role is not authorized
 to perform: s3:GetObject on resource:
 arn:aws:s3:::my-prod-bucket/configs/app.json
 with an explicit deny in a service control policy

Your application cannot read from S3. If this is a config file fetch at startup, your service is failing its health checks and is either crash-looping or serving stale/no data. If it's a data pipeline, your ETL is silently dropping records.

The presence of "Action": "*" in an identity policy does not override an explicit Deny anywhere in the policy evaluation chain (SCP → Permission Boundary → Identity Policy → Resource Policy). AWS evaluation logic is: explicit Deny always wins.


The Attack Vector / Blast Radius

This misconfiguration is a two-sided disaster.

Side 1 — The immediate outage: Your wildcard policy likely conflicts with an Organization-level SCP that explicitly denies s3:* or * on non-approved buckets. The SCP wins. Your app role gets nothing.

Side 2 — The critical security exposure (when there is no SCP): If this role does have "Action": "*", "Resource": "*" without an SCP guardrail, you have handed an attacker a skeleton key to your AWS account. The blast radius:

  • Privilege escalation: iam:CreateAccessKey, iam:AttachUserPolicy, iam:PassRole — attacker creates a backdoor admin user in under 60 seconds.
  • Data exfiltration: Full read access to every S3 bucket, Secrets Manager secret, SSM Parameter Store value, and RDS snapshot in the account.
  • Persistence: lambda:CreateFunction + iam:PassRole = attacker deploys a persistent backdoor Lambda with an execution role they control.
  • Lateral movement: sts:AssumeRole across every role in the account that trusts this principal.

A compromised EC2 instance, a leaked pod service account token, or a misconfigured OIDC trust is all an attacker needs to pivot from a single compute node to full account takeover. This is a P0 security incident waiting to happen.


How to Fix It (The Solution)

Basic Fix — Scope the Action and Resource

Replace the wildcard policy statement with an explicit least-privilege grant.

{
  "Version": "2012-10-17",
  "Statement": [
    {
-     "Sid": "DangerousWildcardAccess",
-     "Effect": "Allow",
-     "Action": "*",
-     "Resource": "*"
+     "Sid": "AllowAppS3ReadOnly",
+     "Effect": "Allow",
+     "Action": [
+       "s3:GetObject",
+       "s3:GetObjectVersion"
+     ],
+     "Resource": "arn:aws:s3:::my-prod-bucket/configs/*"
    }
  ]
}

Also add the s3:ListBucket permission at the bucket level (not object level) or the SDK will throw a misleading 403 instead of a 404 on missing keys:

+   {
+     "Sid": "AllowBucketList",
+     "Effect": "Allow",
+     "Action": "s3:ListBucket",
+     "Resource": "arn:aws:s3:::my-prod-bucket",
+     "Condition": {
+       "StringLike": {
+         "s3:prefix": ["configs/*"]
+       }
+     }
+   }

Enterprise Best Practice — Attribute-Based Access Control (ABAC) with Condition Keys

For multi-tenant or multi-environment deployments, bind S3 access to principal tags so you never have to update policies as new buckets are created.

{
  "Version": "2012-10-17",
  "Statement": [
    {
-     "Sid": "DangerousWildcardAccess",
-     "Effect": "Allow",
-     "Action": "*",
-     "Resource": "*"
+     "Sid": "ABACTaggedS3ReadAccess",
+     "Effect": "Allow",
+     "Action": [
+       "s3:GetObject",
+       "s3:GetObjectVersion",
+       "s3:GetObjectTagging"
+     ],
+     "Resource": "arn:aws:s3:::*/*",
+     "Condition": {
+       "StringEquals": {
+         "aws:ResourceTag/Environment": "${aws:PrincipalTag/Environment}",
+         "aws:ResourceTag/AppName": "${aws:PrincipalTag/AppName}"
+       },
+       "StringEqualsIfExists": {
+         "aws:RequestedRegion": ["us-east-1", "us-west-2"]
+       }
+     }
    }
  ]
}

Tag your IAM role: Environment=production, AppName=my-app. Tag your S3 bucket with matching tags. The policy self-enforces without hardcoding ARNs. This is how you scale IAM without drowning in policy versions.


💡 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 production. Gate it at the PR level.

1. Checkov (Terraform/CloudFormation IaC scanning)

# Install and run against your IaC directory
pip install checkov
checkov -d ./terraform --check CKV_AWS_40,CKV_AWS_289,CKV_AWS_355
# CKV_AWS_40:  IAM policies should not have statements with admin permissions
# CKV_AWS_289: No wildcard resource on write actions
# CKV_AWS_355: No wildcard resource on sensitive read actions

2. OPA/Conftest policy (enforce in GitHub Actions)

# policy/iam_no_wildcard_actions.rego
package aws.iam

deny[msg] {
  stmt := input.resource.aws_iam_policy.properties.policy.Statement[_]
  stmt.Effect == "Allow"
  stmt.Action == "*"
  msg := sprintf("IAM policy '%v' contains wildcard Action '*'. Scope to explicit actions.", [input.resource.aws_iam_policy.name])
}

deny[msg] {
  stmt := input.resource.aws_iam_policy.properties.policy.Statement[_]
  stmt.Effect == "Allow"
  stmt.Resource == "*"
  not contains_condition(stmt)
  msg := sprintf("IAM policy '%v' uses wildcard Resource '*' without a Condition block.", [input.resource.aws_iam_policy.name])
}

contains_condition(stmt) {
  _ = stmt.Condition
}
# .github/workflows/iac-security.yml
- name: Conftest IAM Policy Check
  run: |
    conftest test ./terraform/iam --policy ./policy/

3. AWS Config Rule (runtime enforcement)

Enable the managed rule iam-no-inline-policy-check and deploy a custom Config rule using the Lambda evaluator pattern to flag any policy document containing "Action": "*" on "Resource": "*" without a Condition block. Pair with AWS Security Hub for centralized findings.

4. Permission Boundaries as a hard stop

Attach a Permission Boundary to every role created by your CI/CD pipeline. Even if a wildcard policy is accidentally attached later, the boundary caps the maximum effective permissions:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
    "Resource": "arn:aws:s3:::my-prod-bucket*"
  }]
}

Set this boundary at the AWS Organization level via SCP so no developer or pipeline can create a role without it.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →