Skip to content

Fix: AWS Access Denied — IAM Permission Errors and Policy Debugging

FixDevs ·

Quick Answer

How to fix AWS Access Denied errors — understanding IAM policies, using IAM policy simulator, fixing AssumeRole errors, resource-based policies, and SCPs blocking actions.

The Error

An AWS API call or CLI command returns an access denied error:

An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:iam::123456789012:user/deployer
is not authorized to perform: s3:PutObject
on resource: arn:aws:s3:::my-bucket/uploads/file.txt

Or an IAM role assumption fails:

An error occurred (AccessDenied) when calling the AssumeRole operation:
User: arn:aws:iam::123456789012:user/ci-runner
is not authorized to perform: sts:AssumeRole
on resource: arn:aws:iam::999999999999:role/DeployRole

Or an EC2 instance/Lambda function can’t access other AWS services:

botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the
GetSecretValue operation: User: arn:aws:sts::123456789012:assumed-role/MyRole/MyFunction
is not authorized to perform: secretsmanager:GetSecretValue

Why This Happens

AWS uses a “default deny” security model — all actions are denied unless explicitly permitted. The full access decision requires checking:

  1. Identity-based policies — permissions attached to the user, role, or group making the request
  2. Resource-based policies — permissions on the resource itself (S3 bucket policy, KMS key policy, SQS queue policy)
  3. Permission boundaries — max permissions a role can have (overrides identity policies)
  4. Service control policies (SCPs) — org-level guardrails (AWS Organizations)
  5. Session policies — permissions passed when assuming a role

Any explicit Deny in any policy overrides all Allow statements. Missing a required permission in any layer causes AccessDenied.

Common specific causes:

  • Wrong ARN in the policy — a typo in the resource ARN means the policy matches a different resource than intended
  • Missing trailing /* for S3s3:PutObject on arn:aws:s3:::my-bucket only grants access to the bucket itself, not objects inside it. Objects need arn:aws:s3:::my-bucket/*
  • Cross-account role assumption — the trust policy on the target role must explicitly allow the source account/user to assume it
  • SCP blocking the action — an org-level SCP can block actions even if the IAM policy allows them
  • Resource policy denies — an S3 bucket policy with Effect: Deny overrides any Allow in the user’s identity policy
  • Condition key mismatch — a policy with conditions (MFA required, specific IP range, specific tag) fails silently when conditions aren’t met

Fix 1: Read the Error Message Carefully

The AccessDenied error contains the information needed to diagnose the issue:

User: arn:aws:iam::123456789012:user/deployer
is not authorized to perform: s3:PutObject
on resource: arn:aws:s3:::my-bucket/uploads/file.txt

This tells you:

  • Who: arn:aws:iam::123456789012:user/deployer — the IAM principal making the request
  • What: s3:PutObject — the API action that was denied
  • Where: arn:aws:s3:::my-bucket/uploads/file.txt — the specific resource

Check what policies are attached to this user:

# List policies attached to the user
aws iam list-attached-user-policies --user-name deployer

# List inline policies
aws iam list-user-policies --user-name deployer

# List groups the user belongs to (group policies also apply)
aws iam list-groups-for-user --user-name deployer

Fix 2: Use the IAM Policy Simulator

The IAM Policy Simulator lets you test whether a specific action would be allowed or denied without making the actual API call:

From the AWS Console:

  1. Go to IAMPolicy Simulator (or search “IAM Policy Simulator”)
  2. Select the user, role, or group to test
  3. Choose the service (e.g., S3) and action (e.g., PutObject)
  4. Enter the resource ARN
  5. Click Run Simulation

From the CLI:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:user/deployer \
  --action-names s3:PutObject \
  --resource-arns "arn:aws:s3:::my-bucket/uploads/file.txt"
{
  "EvaluationResults": [
    {
      "EvalActionName": "s3:PutObject",
      "EvalDecision": "implicitDeny",
      "MatchedStatements": [],
      "MissingContextValues": []
    }
  ]
}

"implicitDeny" = no policy allows this action (add a policy). "explicitDeny" = a policy explicitly denies this action (find and remove the Deny).

Fix 3: Fix S3 Bucket and Object Permission Mistakes

S3 has two levels of ARNs — the bucket and the objects inside it. Most actions require different permissions for each:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",           // Needs bucket ARN
        "s3:GetBucketLocation"     // Needs bucket ARN
      ],
      "Resource": "arn:aws:s3:::my-bucket"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",            // Needs object ARN (bucket/*)
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-bucket/*"  // /* is required for objects
    }
  ]
}

Common Mistake: Using "Resource": "arn:aws:s3:::my-bucket" (without /*) for s3:PutObject. This matches the bucket itself but not any objects. The action is denied because no policy matches arn:aws:s3:::my-bucket/uploads/file.txt.

S3 bucket policy blocking external access:

// Bucket policy — might be blocking your user's access
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalAccount": "123456789012"   // Only allows account 123...
        }
      }
    }
  ]
}
// If your user is in a different account, this Deny overrides their Allow

Fix 4: Fix IAM Role Trust Policies for AssumeRole

When a user or service needs to assume an IAM role, the role’s trust policy must allow it:

// The role's trust policy (who can assume this role)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:user/ci-runner"  // Specific user
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Common trust policy patterns:

// Allow an entire account to assume the role
{
  "Principal": { "AWS": "arn:aws:iam::123456789012:root" }
}

// Allow a specific role (e.g., CI/CD system role)
{
  "Principal": { "AWS": "arn:aws:iam::123456789012:role/GitHubActionsRole" }
}

// Allow EC2 instances to assume the role (for instance profiles)
{
  "Principal": { "Service": "ec2.amazonaws.com" }
}

// Allow Lambda to assume the role (execution role)
{
  "Principal": { "Service": "lambda.amazonaws.com" }
}

// Cross-account — allow account 999 to assume a role in account 123
{
  "Principal": { "AWS": "arn:aws:iam::999999999999:root" }
}

Also, the user/role assuming the role must have sts:AssumeRole permission:

// Policy on the user/role doing the assuming
{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::999999999999:role/DeployRole"
}

Both sides must allow the operation: the trust policy on the target role AND an sts:AssumeRole permission on the source principal.

Fix 5: Fix Permissions for EC2 and Lambda

EC2 instances and Lambda functions use instance profiles and execution roles for AWS API access. If these aren’t configured correctly, AWS SDK calls fail with AccessDenied:

Lambda — attach a role with the right permissions:

# Check the Lambda function's execution role
aws lambda get-function-configuration \
  --function-name my-function \
  --query 'Role'
# Returns: "arn:aws:iam::123456789012:role/MyLambdaRole"

# Check what policies are attached to that role
aws iam list-attached-role-policies --role-name MyLambdaRole

# Add a managed policy to the role
aws iam attach-role-policy \
  --role-name MyLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite

Minimal Lambda role for Secrets Manager access:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-??????"
      // The ?????? matches the 6-char suffix AWS adds to secret ARNs
    }
  ]
}

EC2 — attach an instance profile:

# Create a role with EC2 trust policy
aws iam create-role \
  --role-name MyEC2Role \
  --assume-role-policy-document file://ec2-trust-policy.json

# Attach permissions
aws iam attach-role-policy \
  --role-name MyEC2Role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

# Create instance profile and add role
aws iam create-instance-profile --instance-profile-name MyEC2Profile
aws iam add-role-to-instance-profile \
  --instance-profile-name MyEC2Profile \
  --role-name MyEC2Role

# Attach to running instance
aws ec2 associate-iam-instance-profile \
  --instance-id i-1234567890abcdef0 \
  --iam-instance-profile Name=MyEC2Profile

Fix 6: Debug Deny from SCPs or Permission Boundaries

If IAM policies look correct but the error persists, a Service Control Policy (SCP) or Permission Boundary may be blocking the action:

# Check if SCPs apply to your account
aws organizations list-policies-for-target \
  --target-id <ACCOUNT_ID> \
  --filter SERVICE_CONTROL_POLICY

# Get the SCP document
aws organizations describe-policy --policy-id p-xxxxxxxxxxxx

SCPs can’t be overridden by IAM policies — an SCP Deny blocks the action regardless of what any IAM policy says.

Permission boundaries — check if a role has a permission boundary:

aws iam get-role --role-name MyRole --query 'Role.PermissionsBoundary'

A permission boundary limits the maximum permissions a role can have. If the boundary doesn’t include secretsmanager:GetSecretValue, the role can’t perform that action even if the role’s policy allows it.

Enable CloudTrail to see the exact denial reason:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
  --start-time 2026-03-21T00:00:00Z \
  --end-time 2026-03-21T23:59:59Z \
  --query 'Events[?contains(CloudTrailEvent, `AccessDenied`)]'

CloudTrail logs include errorCode and errorMessage fields that show whether the denial came from an SCP, a permission boundary, or a resource policy.

Fix 7: Common Patterns for GitHub Actions and CI/CD

GitHub Actions deployments commonly hit IAM issues. The recommended approach uses OIDC (no long-lived credentials):

# .github/workflows/deploy.yml
permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1

IAM role trust policy for GitHub Actions OIDC:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
          // Replace with your org/repo — limits which repos can assume this role
        }
      }
    }
  ]
}

Still Not Working?

Enable AWS CloudTrail if not already active — it logs all API calls including denied ones, showing exactly which policy caused the denial.

Check resource ARN wildcards — some services require exact ARNs, others support wildcards:

// DynamoDB — table and stream ARNs are separate
"Resource": [
  "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable",
  "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable/stream/*"
]

// KMS — must specify both the key and its alias
"Resource": [
  "arn:aws:kms:us-east-1:123456789012:key/key-id",
  "arn:aws:kms:us-east-1:123456789012:alias/my-key"
]

Wait for IAM propagation — IAM changes take up to a few seconds to propagate globally. If you just attached a policy, wait 10–30 seconds and retry.

For related AWS issues, see Fix: GitHub Actions Permission Denied and Fix: AWS S3 Access Denied.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles