Skip to content
AWS API Gateway 📅 2026-02-02

Troubleshoot Access Denied (403 Forbidden) Error in AWS API Gateway

When deploying or operating services behind AWS API Gateway, encountering a 403 Forbidden error is a common authorization issue. As an SRE, diagnosing and resolving this production-impacting error swiftly requires a systematic approach, often leveraging the AWS CLI and CloudWatch logs. This guide provides a direct path to identifying and rectifying the underlying causes.

🚨 Symptoms & Diagnosis

A 403 Forbidden error indicates that the request reached API Gateway, but the client was denied access to the requested resource. This can manifest in several ways:

HTTP 403 Forbidden
{
  "message": "Forbidden"
}

Or, with more detail often found in logs or specific configurations:

{
  "authorize-status": "403",
  "authorize-error": "The client is not authorized to perform this operation."
}
{
  "authenticate-status": "200",
  "authorize-status": "403"
}

Root Cause: The 403 Forbidden typically arises from an authorization failure within the AWS API Gateway Authorization Pipeline. This means either an explicit Deny statement or an implicit Deny due to a missing Allow permission in an associated IAM policy, resource policy, or authorizer logic. Invalid or expired credentials can also lead to an authorization failure if not correctly handled by the chosen authorizer.


🛠️ Solutions

Immediate Mitigation: Enable CloudWatch Access Logs

Immediate Mitigation: Enable CloudWatch Access Logs

This quick fix allows you to capture detailed phase-specific errors in CloudWatch, providing crucial insights into whether the authentication or authorization phase failed. This is your first step in any live incident to understand the context of the denial.

  1. Retrieve your API's ID and target stage.
  2. Update the API Gateway stage to enable access logging, directing logs to a specified CloudWatch Log Group.
  3. Configure a detailed log format to capture authenticate-status and authorize-status.
  4. Invoke your API to trigger the error, then tail the CloudWatch logs to identify the failing phase.
# Define API_ID and STAGE (replace 'YourApiName' and 'prod' as necessary)
API_ID=$(aws apigateway get-rest-apis --query 'items[?name==`YourApiName`].id' --output text)
STAGE=prod

# Ensure the log group exists, or create it if not
LOG_GROUP_ARN="arn:aws:logs:us-east-1:123456789012:log-group:/aws/apigateway/$API_ID/$STAGE" # Replace 123456789012 with your AWS Account ID
aws logs create-log-group --log-group-name "/aws/apigateway/$API_ID/$STAGE" --region us-east-1

# Update API Gateway stage to enable access logging with a detailed format
aws apigateway update-stage \
  --rest-api-id $API_ID \
  --stage-name $STAGE \
  --patch-operations \
    op=replace,path=/accessLogSettings/destinationArn,value=$LOG_GROUP_ARN \
    op=replace,path=/accessLogSettings/format,value='{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","caller":"$context.identity.caller","user":"$context.identity.user","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","authenticate-status":"$context.authorizer.integrationStatus","authorize-status":"$context.authorizer.status"}'

echo "Access logging enabled for API: $API_ID, Stage: $STAGE. Now invoke your API to generate logs."

# Tail the logs (Ctrl+C to stop)
aws logs tail /aws/apigateway/$API_ID/$STAGE --follow

Best Practice Fix: Audit and Fix IAM Permissions & Resource Policy

Best Practice Fix: Audit and Fix IAM Permissions & Resource Policy

This permanent solution involves systematically auditing and correcting the IAM policies attached to the invoking identity and the API Gateway resource policy itself. Granting least privilege is critical for security posture.

  1. Check IAM Role/Policy: Verify that the IAM user or role attempting to invoke the API has an Allow statement for execute-api:Invoke on the specific API Gateway resource (ARN).
  2. Inspect Resource Policy: Examine the API Gateway's resource policy for any explicit Deny statements that might override Allow permissions.
  3. Test with AWS CLI Signed Request: Use the aws apigateway test-invoke-method command to simulate a signed request, which bypasses common client-side authentication issues and focuses on policy evaluation.
  4. Update Policy and Retest: Based on your findings, modify the relevant IAM or resource policy and retest the API invocation.
# Check API resource policy for explicit Deny statements
# Replace $API_ID with the actual API ID
aws apigateway get-rest-api --rest-api-id $API_ID --query 'policy'

# Example: Attach an IAM policy allowing execute-api:Invoke for a specific role
# Replace 'YourRole', 'us-east-1', '123456789012', and 'YourApiName' placeholders.
# The resource ARN should be as specific as possible.
# For example, to allow GET on any resource under a specific API ID and stage:
# "arn:aws:execute-api:us-east-1:123456789012:$API_ID/*/GET/*"
# Or for a specific resource path:
# "arn:aws:execute-api:us-east-1:123456789012:$API_ID/prod/yourpath/GET"

cat > temp-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:us-east-1:123456789012:$API_ID/*/GET/*"
  }]
}
EOF
aws iam put-role-policy --role-name YourRole --policy-name GatewayInvoke --policy-document file://temp-policy.json

# Test signed invoke directly via API Gateway (bypasses network issues)
# Replace $API_ID, resource ID, http-method, and path as applicable
# To get resource ID: aws apigateway get-resources --rest-api-id $API_ID
aws apigateway test-invoke-method \
  --rest-api-id $API_ID \
  --resource-id $(aws apigateway get-resources --rest-api-id $API_ID --query 'items[?path==`/yourpath`].id' --output text) \
  --http-method GET \
  --body '{}' \
  --header-in 'Content-Type=application/json'

Debugging Lambda Authorizer Issues

If your API utilizes a Lambda authorizer, a 403 Forbidden can indicate that the authorizer itself is rejecting the request.

  1. Check Authorizer Logs: Review the CloudWatch logs for your Lambda authorizer function. Look for DENY messages or errors in your custom authorizer logic.
  2. Verify DENY Response: Confirm that your authorizer is explicitly returning a DENY policy or throwing an unhandled error.
  3. Update Authorizer Logic: Adjust the Lambda function's logic or token validation process to correctly authorize requests.
# Filter Lambda authorizer logs for DENY responses
# Replace 'YourAuthorizer' with your Lambda authorizer function name
aws logs filter-log-events \
  --log-group-name /aws/lambda/YourAuthorizer \
  --filter-pattern 'DENY' \
  --start-time $(date -d '1 hour ago' +%s000)

# Test authorizer directly with a sample payload
# Replace 'YourAuthorizer' and 'test-token'
aws lambda invoke \
  --function-name YourAuthorizer \
  --payload '{"type":"TOKEN","authorizationToken":"test-token","methodArn":"arn:aws:execute-api:us-east-1:123456789012:$API_ID/prod/GET/yourpath"}' \
  output.json \
  && cat output.json

🧩 Technical Context (Visualized)

The AWS API Gateway authorization pipeline is a multi-stage process that handles incoming requests. An Access Denied (403 Forbidden) error typically occurs during the Authorization Phase, after Authentication (if configured) has succeeded. This pipeline evaluates IAM policies, Lambda authorizer responses, and API Gateway resource policies to determine if a client is permitted to invoke a specific method on a resource.

graph TD
    A[Client Request] --> B{API Gateway};
    B -- Request Header/Body --> C{Authentication Phase};
    C -- Valid Credentials --> D{Authorization Phase};
    C -- Invalid/Expired Credentials --> E[401 Unauthorized / 403 Forbidden];
    D -- IAM Policy Allow --> F[Backend Integration (e.g., Lambda, HTTP Endpoint)];
    D -- Lambda Authorizer Allow --> F;
    D -- Resource Policy Allow --> F;
    D -- IAM Policy Deny --> E;
    D -- Lambda Authorizer Deny --> E;
    D -- Resource Policy Deny --> E;
    F --> G[Client Response (200 OK)];
    E -- Error Response --> G;

    style E fill:#f9f,stroke:#333,stroke-width:2px;
    linkStyle 4 stroke:#f00,stroke-width:2px,fill:none;
    linkStyle 5 stroke:#f00,stroke-width:2px,fill:none;
    linkStyle 6 stroke:#f00,stroke-width:2px,fill:none;
    click E "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-controlling-access.html" "API Gateway Authorization Documentation"

✅ Verification

After implementing any of the solutions, it's critical to verify that the issue is resolved and the API is accessible.

  1. Direct API Gateway Test Invoke:

    # Replace $API_ID, RESOURCE_ID, and http-method
    aws apigateway test-invoke-method \
      --rest-api-id $API_ID \
      --resource-id RESOURCE_ID \
      --http-method GET \
      --body '{}'
    

  2. External curl Command (with Authorization if applicable):

    # Replace $API_ID and your path. Include Authorization header if your API requires it.
    curl -X GET "https://$API_ID.execute-api.us-east-1.amazonaws.com/prod/yourpath" \
      -H 'Authorization: Bearer valid-token' -v | grep -i status
    

  3. CloudWatch Logs for Success:

    # Verify that successful requests (status:200) are now appearing in your access logs.
    # Replace $API_ID and stage ('prod').
    aws logs filter-log-events \
      --log-group-name /aws/apigateway/$API_ID/prod \
      --filter-pattern 'status:200' \
      --limit 1
    

📦 Prerequisites

To effectively troubleshoot using the methods outlined in this guide, ensure you have the following in place:

  • AWS CLI v2.13+: Configured with credentials for an IAM principal.
  • jq: A lightweight and flexible command-line JSON processor.
  • Admin IAM Role: Or an IAM role with sufficient permissions (e.g., apigateway:*, logs:*, iam:*, sts:GetCallerIdentity) to inspect and modify API Gateway, CloudWatch Logs, and IAM resources.
  • CloudWatch Logs Enabled (for specific solutions): While part of the quick fix, having it readily available improves debuggability.