← Back to Blog

AWS IAM Access Denied: A Systematic Approach to Fixing Permission Errors

You run an AWS CLI command and get An error occurred (AccessDenied). Now what? I have debugged hundreds of these errors across production accounts, and there is a repeatable process that resolves them every time. This guide walks through each step, with real commands and real policy JSON you can use immediately.

TL;DR

Run aws sts get-caller-identity first. Then check these in order: IAM policy (identity-based), resource policy, SCPs, permissions boundary. Use CloudTrail with errorCode: AccessDenied to find the exact denied action and resource. Decode encoded error messages with aws sts decode-authorization-message to get the full picture.

Why IAM Returns Access Denied

Before jumping into fixes, you need to understand how AWS evaluates permissions. Every API call goes through a policy evaluation engine that checks up to six different policy types. The evaluation follows three rules:

  1. Explicit deny always wins. If any policy says "Effect": "Deny" for the action, the request is denied. Period. No other policy can override it.
  2. Explicit allow is required. At least one policy must explicitly allow the action. The absence of an allow is itself a denial.
  3. Implicit deny is the default. If no policy mentions the action at all, the request is denied.

This means your request can be denied for two very different reasons: something is explicitly blocking it, or nothing is explicitly allowing it. The debugging approach differs depending on which case you are dealing with.

Here are the six policy types that can deny a request:

  • Identity-based policies (IAM user, group, or role policies)
  • Resource-based policies (S3 bucket policies, KMS key policies, Lambda function policies)
  • Service Control Policies (SCPs) (AWS Organizations level)
  • Permissions boundaries (IAM entity level ceiling)
  • Session policies (passed during AssumeRole or federation)
  • Access Control Lists (ACLs) (legacy S3 and VPC ACLs)

When you get an Access Denied error, the problem is in one (or more) of these six layers. I will walk through each one systematically.

Step 1: Who Am I?

The single most common cause of Access Denied errors is not being who you think you are. Before checking any policy, verify your identity:

aws sts get-caller-identity

This returns three critical pieces of information:

{
    "UserId": "AROA3XFRBF23CLEXAMPLE:session-name",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/MyRole/session-name"
}

Look carefully at the output. Common surprises include:

  • Wrong account. You are in account 111111111111 but the resource is in 222222222222. This happens constantly when you have multiple AWS profiles configured.
  • Assumed role instead of IAM user. Your shell assumed a role (perhaps through an SSO session or instance profile) and that role has different permissions than your IAM user.
  • Expired session. STS temporary credentials have a maximum duration. If your session expired, you will get Access Denied rather than an explicit expiration error in some services.
  • Wrong profile. You forgot to pass --profile production and the default profile points to a sandbox account.

I cannot overstate how often the fix is simply export AWS_PROFILE=correct-profile. Check this first, every time.

Step 2: Decode the Error Message

Some AWS services return encoded authorization failure messages. These encoded messages contain the exact details of why the request was denied, but you need to decode them. When you see an error like this:

An error occurred (UnauthorizedOperation) when calling the RunInstances operation:
You are not authorized to perform this operation. Encoded authorization failure message:
fSp3b1hFn_Kw6Xsd7gF3cN...

Decode it with:

aws sts decode-authorization-message --encoded-message "fSp3b1hFn_Kw6Xsd7gF3cN..." --query DecodedMessage --output text | python3 -m json.tool

The decoded output gives you the full picture:

{
    "allowed": false,
    "explicitDeny": false,
    "matchedStatements": {
        "items": []
    },
    "failures": {
        "items": []
    },
    "context": {
        "principal": {
            "id": "AROA3XFRBF23CLEXAMPLE:session-name",
            "arn": "arn:aws:sts::123456789012:assumed-role/MyRole/session-name"
        },
        "action": "ec2:RunInstances",
        "resource": "arn:aws:ec2:us-east-1:123456789012:instance/*",
        "conditions": {
            "items": [
                {
                    "key": "ec2:InstanceType",
                    "values": { "items": [{ "value": "m5.xlarge" }] }
                }
            ]
        }
    }
}

This tells you exactly which principal attempted which action on which resource, with which conditions. The "explicitDeny": false field is particularly important. It tells you whether the denial came from an explicit deny statement or from the absence of an allow (implicit deny). If explicitDeny is true, search for deny statements. If false, you are missing an allow.

Important: The caller must have sts:DecodeAuthorizationMessage permission to run this command. If you cannot decode the message, ask an admin to do it or check CloudTrail.

Step 3: Check Identity-Based Policies

Identity-based policies are attached to the IAM entity (user, group, or role) making the request. There are three types to check:

  • Inline policies embedded directly on the user or role
  • Managed policies attached to the user or role
  • Group policies inherited from IAM groups (for IAM users only, not roles)

Rather than manually reading every policy, use the IAM policy simulator:

# Simulate whether a specific principal can perform an action
aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:role/MyRole \
    --action-names s3:GetObject \
    --resource-arns arn:aws:s3:::my-bucket/my-key

# Output includes:
# EvalActionName: s3:GetObject
# EvalDecision: implicitDeny | explicitDeny | allowed
# MatchedStatements: (which policy statement matched)

The EvalDecision field tells you the result. If it says implicitDeny, you need to add an allow statement. If it says explicitDeny, you need to find and remove or modify the deny statement.

To list all policies attached to a role:

# List attached managed policies
aws iam list-attached-role-policies --role-name MyRole

# List inline policies
aws iam list-role-policies --role-name MyRole

# Get the actual policy document for an inline policy
aws iam get-role-policy --role-name MyRole --policy-name MyInlinePolicy

For IAM users, also check group memberships:

# List groups the user belongs to
aws iam list-groups-for-user --user-name myuser

# Check each group's policies
aws iam list-attached-group-policies --group-name MyGroup
aws iam list-group-policies --group-name MyGroup

Step 4: Check Resource-Based Policies

Resource-based policies are attached to the resource being accessed, not to the caller. Many AWS services support them:

  • S3 bucket policies control who can access the bucket and its objects
  • KMS key policies control who can use the encryption key
  • Lambda function policies control who can invoke the function
  • SQS queue policies, SNS topic policies, Secrets Manager resource policies

To check an S3 bucket policy:

aws s3api get-bucket-policy --bucket my-bucket --query Policy --output text | python3 -m json.tool

Look for explicit deny statements that might be blocking your principal. A common pattern is a bucket policy that denies access unless the request comes from a specific VPC endpoint:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyNonVpcAccess",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::my-bucket",
                "arn:aws:s3:::my-bucket/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpce": "vpce-1a2b3c4d"
                }
            }
        }
    ]
}

This policy denies all S3 actions unless the request comes through the specified VPC endpoint. If you are calling from your laptop or a different VPC, you will get Access Denied even if your IAM policy allows the action.

The Cross-Account Double Allow Rule

For cross-account access, both the identity policy in the caller's account AND the resource policy in the resource's account must allow the action. This is the "double allow" rule. If either side is missing the allow, the request fails.

There is one exception: if the resource policy grants access to a specific IAM principal (not just the account), and the resource and caller are in different accounts, the resource policy alone is sufficient for some services (like S3). But relying on this exception is fragile. Always configure both sides.

Pro Tip: IAM Access Analyzer Policy Validation

AWS IAM Access Analyzer can validate your policy JSON and tell you exactly what is wrong. Go to the IAM console, select "Access Analyzer" in the left nav, then "Policy validation." Paste your policy JSON and it will flag errors, security warnings, and suggestions. It catches issues like invalid ARN formats, unsupported condition keys, misspelled action names, and overly permissive wildcards. You can also run this from the CLI with aws accessanalyzer validate-policy. I use this before every policy deployment.

Check If Your AWS Keys Are Exposed

IAM Access Denied might be the least of your problems if your credentials are publicly exposed. Run a free scan to check whether your domain is leaking secrets, API keys, or sensitive configuration files.

Scan for Exposed Secrets

Step 5: Service Control Policies (SCPs)

If you are in an AWS Organization, SCPs can deny actions at the organization, OU, or account level. SCPs are the ultimate override. Even if your IAM policy explicitly allows an action, an SCP deny wins.

SCPs are invisible to normal IAM users. You cannot see them from within the member account. This makes them a common source of mysterious Access Denied errors, especially for actions like creating IAM users, launching specific instance types, or operating in restricted regions.

To check SCPs (requires Organizations read access, typically from the management account):

# List all SCPs in the organization
aws organizations list-policies --filter SERVICE_CONTROL_POLICY

# Get the policy content
aws organizations describe-policy --policy-id p-abcdefgh

# List policies attached to a specific account
aws organizations list-policies-for-target \
    --target-id 123456789012 \
    --filter SERVICE_CONTROL_POLICY

Common SCP patterns that cause Access Denied:

  • Region restriction. SCP denies all actions outside allowed regions (e.g., only us-east-1 and eu-west-1 are permitted).
  • Service restriction. SCP denies access to specific services like IAM, Organizations, or Account.
  • Instance type restriction. SCP only allows specific EC2 instance families.
  • Root account restriction. SCP denies all actions by the root user (a best practice, but surprising when you need root).

If you suspect an SCP is blocking you but cannot access Organizations, check CloudTrail. The event will show errorCode: AccessDenied, and the error message sometimes hints at an SCP denial (though AWS does not always make this explicit).

Step 6: Permissions Boundaries

Permissions boundaries are the most misunderstood IAM feature. A permissions boundary is a managed policy that sets the maximum permissions an IAM entity can have. It does not grant permissions. It limits them.

Think of it as an intersection: the effective permissions are the overlap between the identity policy and the permissions boundary. If the identity policy allows s3:* but the boundary only allows s3:GetObject, the effective permission is only s3:GetObject.

# Check if a role has a permissions boundary
aws iam get-role --role-name MyRole --query "Role.PermissionsBoundary"

# Output if boundary exists:
{
    "PermissionsBoundaryType": "Policy",
    "PermissionsBoundaryArn": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
}

# Get the boundary policy content
aws iam get-policy-version \
    --policy-arn arn:aws:iam::123456789012:policy/DeveloperBoundary \
    --version-id v1 \
    --query "PolicyVersion.Document" --output text | python3 -m json.tool

The intersection trap is the most common issue. I have seen teams spend hours adding more and more permissions to an IAM policy, not realizing the boundary was capping the effective permissions. If you add s3:PutObject to the identity policy but the boundary does not include it, the action is still denied.

To fix a boundary issue, you have two options: add the needed action to the boundary policy, or remove the boundary entirely (if appropriate). Modifying boundaries typically requires elevated permissions, so coordinate with your IAM admin team.

Step 7: Cross-Account Role Assumption

Cross-account access is where IAM errors get most confusing. When you assume a role in another account, two things must be true:

  1. The trust policy on the target role must allow your principal to assume it.
  2. Your identity policy must allow the sts:AssumeRole action on the target role ARN.

Here is a correct trust policy on the target role in account 222222222222:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/CallerRole"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringEquals": {
                    "sts:ExternalId": "my-external-id-12345"
                }
            }
        }
    ]
}

And the corresponding identity policy in account 111111111111:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::222222222222:role/TargetRole"
        }
    ]
}

Common failure points:

  • Principal ARN mismatch. The trust policy expects arn:aws:iam::111111111111:root but you are calling from a specific role. Use the specific role ARN in the principal.
  • Missing ExternalId. If the trust policy requires an ExternalId condition, you must pass it in the AssumeRole call: --external-id my-external-id-12345.
  • Confused deputy problem. Without an ExternalId, any principal in the trusted account can assume the role. This is a security risk. Always use ExternalId for third-party access.
  • Session duration exceeds maximum. The trust policy can set a MaxSessionDuration. If your AssumeRole call requests a longer duration, the request fails.

IAM Policy Types Comparison

Policy Type Scope Can Deny Can Allow Common Use Case
Identity-based IAM user, group, or role Yes Yes Grant permissions to a specific entity
Resource-based The resource itself Yes Yes Cross-account S3 access, KMS key sharing
SCP AWS Organization / OU / Account Yes Yes (guardrail) Restrict entire accounts from certain services or regions
Permissions Boundary IAM user or role Yes (implicit) No (only limits) Delegate IAM admin while capping maximum permissions
Session Policy Temporary session (AssumeRole, federation) Yes (implicit) No (only limits) Scope down temporary credentials for specific tasks
ACL S3 bucket/object, VPC subnet Yes Yes Legacy cross-account S3 access (prefer bucket policies)

The Cost of IAM Misconfigurations

IAM misconfigurations are not just an operational annoyance. They are a security risk. Overly permissive policies are the number one cause of cloud security breaches. According to Gartner, through 2025, 99% of cloud security failures will be the customer's fault, primarily due to IAM misconfigurations.

The financial impact is real. An exposed IAM key can lead to cryptocurrency mining charges in the tens of thousands of dollars within hours. A misconfigured S3 bucket policy has caused some of the largest data breaches in history. And an overly permissive role in a CI/CD pipeline can give attackers lateral movement across your entire infrastructure.

Prevention Strategies

  • Least privilege auditing. Use IAM Access Analyzer to find unused permissions. Remove what you do not need. Run aws accessanalyzer create-analyzer --analyzer-name my-analyzer --type ACCOUNT to get started.
  • CloudTrail monitoring. Set up CloudWatch alarms for errorCode: AccessDenied events. A spike in denied requests often indicates an attack or a misconfiguration that needs attention.
  • Regular access reviews. Generate credential reports with aws iam generate-credential-report and review unused keys, users without MFA, and old access keys. Check our AWS Security Checklist for Production for a full audit workflow.
  • Infrastructure as Code. Define IAM policies in Terraform or CloudFormation. Code review catches permission errors before they reach production.
  • Automated key rotation. Never use long-lived IAM access keys when you can use IAM roles with temporary credentials. For cases where keys are unavoidable, rotate them regularly. Our guide on checking if your API key is exposed covers detection and response.

Is Your Infrastructure Leaking Secrets?

Misconfigured permissions are dangerous, but exposed credentials are catastrophic. Run a free exposure check to see if your domain is leaking environment files, API keys, or configuration secrets right now.

Run Free Exposure Check

Common Mistakes That Cause Access Denied

After years of debugging IAM issues, these are the mistakes I see over and over:

  1. Using the root account for daily operations. Root has no IAM policies, but it can be restricted by SCPs. It also bypasses some IAM checks in unpredictable ways. Use a dedicated IAM role instead.
  2. Wildcard resources with condition keys. Writing "Resource": "*" when the action requires a specific resource ARN. Some actions (like s3:PutObject) require the object ARN, not just the bucket ARN.
  3. Forgetting KMS permissions for encrypted S3. You have s3:GetObject permission, but the object is encrypted with a customer-managed KMS key. You also need kms:Decrypt on that key. This is one of the most common Access Denied causes for S3.
  4. Not checking SCPs. You spend an hour debugging IAM policies only to discover the organization has an SCP that blocks the service in your region. Always ask: "Are there any SCPs on this account?"
  5. Wrong region. Your IAM policy allows ec2:DescribeInstances with a condition restricting to us-east-1, but you are calling the eu-west-1 endpoint. Region conditions are case-sensitive and must match exactly.
  6. S3 ARN format errors. The bucket ARN is arn:aws:s3:::my-bucket (no region, no account). The object ARN is arn:aws:s3:::my-bucket/*. Many policies only include the bucket ARN and miss the object ARN, causing GetObject and PutObject to fail.
  7. Assuming managed policies are enough. AWS managed policies like AmazonS3ReadOnlyAccess grant broad read access, but they do not cover KMS decryption, VPC endpoint conditions, or resource-based denies. Do not assume a managed policy handles everything.

Frequently Asked Questions

How do I find which exact policy is denying my request?

Start with CloudTrail. Search for events with "errorCode": "AccessDenied" or "Client.UnauthorizedOperation". The event record includes the principal ARN, action, resource, and timestamp. Then use aws iam simulate-principal-policy to test the action against the principal. If that shows allowed but the real request fails, the denial is coming from a resource policy, SCP, or permissions boundary. Decode encoded error messages with aws sts decode-authorization-message for the most detailed information.

Why does Access Denied sometimes say "AccessDenied" and sometimes "UnauthorizedOperation"?

Different AWS services use different error codes. S3 returns AccessDenied. EC2 returns UnauthorizedOperation. STS returns AccessDenied. The underlying cause is the same: the IAM policy evaluation denied the request. The error code is service-specific, not a different type of denial.

Can I use CloudTrail to debug Access Denied errors in real time?

CloudTrail events have a delay of up to 15 minutes for management events and longer for data events (like S3 object-level operations). For faster debugging, use CloudTrail Lake with event data stores, which provides near-real-time query capability. Alternatively, stream CloudTrail to CloudWatch Logs and set up a metric filter for errorCode = "AccessDenied" to trigger alarms.

What is the difference between an implicit deny and an explicit deny?

An implicit deny occurs when no policy grants the requested permission. It is the default state for all actions. An explicit deny occurs when a policy statement has "Effect": "Deny" matching the action. The critical difference: an explicit deny cannot be overridden by any allow statement. An implicit deny can be resolved by adding an allow. When debugging, this distinction determines whether you need to add a permission or remove a restriction.

How do I troubleshoot Access Denied for a Lambda function accessing other AWS services?

Lambda functions run with an execution role. First, identify the role: check the Lambda function configuration for its execution role ARN. Then check that role's identity policies for the needed permissions. Common issues include missing kms:Decrypt for encrypted resources, missing VPC-related permissions (ec2:CreateNetworkInterface) for VPC-attached Lambdas, and resource-based policies on the target service that do not allow the Lambda role. Use aws lambda get-function --function-name my-func --query Configuration.Role to find the role, then debug it with the steps in this guide.

The Bottom Line

AWS IAM Access Denied errors are frustrating, but they are always solvable with a systematic approach. Start with identity verification (get-caller-identity), decode the error message, then work through identity policies, resource policies, SCPs, permissions boundaries, and cross-account trust in order. Do not guess. Use the CLI commands in this guide to get concrete answers at each step.

The investment in understanding IAM policy evaluation pays off permanently. Once you internalize the evaluation logic (explicit deny beats explicit allow beats implicit deny), you can diagnose most issues in minutes rather than hours.

Related guides: AWS Security Checklist for Production, How to Check If Your API Key Is Exposed, Exposure Checker Tool, and 70+ more free tools.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.