Why Is My Lambda Function Returning a 403 When Calling S3? Fix the Execution Role
Your Lambda function executes cleanly — no timeout, no crash — but every S3 API call comes back with a 403 Forbidden. This is one of the most common permission gaps engineers hit when wiring up Lambda to S3 for the first time, and the root cause is almost never where you first look.
TL;DR: Lambda 403 on S3 — Root Causes at a Glance
| Root Cause | Layer | Fix |
|---|---|---|
Missing s3:GetObject on execution role | IAM Role | Add S3 read action to role policy |
| Bucket policy explicitly denies the role ARN | S3 Bucket Policy | Remove or adjust the Deny statement |
| Bucket policy restricts access to specific principals and omits the role | S3 Bucket Policy | Add the execution role ARN to the Allow principal |
| SCP blocks S3 actions at the account or OU level | AWS Organizations SCP | Review and update the SCP with your org admin |
| Server-side encryption with a customer-managed KMS key | KMS Key Policy | Grant kms:Decrypt to the execution role |
| VPC endpoint policy restricts S3 access | VPC Endpoint Policy | Update the endpoint policy to allow the role and bucket |
How Lambda S3 Authorization Actually Works
When your Lambda function calls s3:GetObject, AWS evaluates multiple independent policy layers before returning a response. The request must pass every applicable layer — a single explicit Deny anywhere in the chain overrides all Allow statements. Engineers routinely fix the IAM role and still get a 403 because a bucket policy or KMS key policy is blocking the request at a different layer. Understanding the evaluation order is the only way to diagnose this systematically.
(AWS Organizations)"] B -- "Deny" --> Z1["403 Forbidden"] B -- "No Deny" --> C["IAM Execution Role Policy"] C -- "No Allow / Explicit Deny" --> Z2["403 Forbidden"] C -- "Allow" --> D["S3 Bucket Policy"] D -- "Explicit Deny" --> Z3["403 Forbidden"] D -- "Allow or No Policy
(same-account)" --> E["KMS Key Policy
(if SSE-KMS enabled)"] E -- "No kms:Decrypt" --> Z4["403 Forbidden"] E -- "Allow" --> F["VPC Endpoint Policy
(if gateway endpoint present)"] F -- "Deny or Missing Allow" --> Z5["403 Forbidden"] F -- "Allow" --> G["✅ 200 OK — Object Returned"] style Z1 fill:#c0392b,color:#fff style Z2 fill:#c0392b,color:#fff style Z3 fill:#c0392b,color:#fff style Z4 fill:#c0392b,color:#fff style Z5 fill:#c0392b,color:#fff style G fill:#27ae60,color:#fff
- SCP (if AWS Organizations is in use): Account-level guardrails evaluated first. If an SCP denies
s3:GetObject, no downstream policy can override it. - IAM Execution Role: The Lambda function assumes this role. The role's identity-based policy must explicitly allow the S3 action and resource.
- S3 Bucket Policy: A resource-based policy attached to the bucket. An explicit Deny here overrides the IAM Allow. A missing Allow on a cross-account bucket also results in a 403.
- KMS Key Policy (if SSE-KMS is enabled): If the object is encrypted with a customer-managed key, the execution role must also be granted
kms:Decryptin the key policy. - VPC Endpoint Policy (if Lambda runs in a VPC with an S3 gateway endpoint): The endpoint policy is an additional filter on traffic routed through the endpoint.
Step 1: Confirm the Lambda Execution Role Identity
Before touching any policy, confirm which IAM role your function is actually using. It is easy to assume the role is what the console shows while a recent deployment or Terraform apply attached a different role. This step closes that gap before you spend time editing the wrong policy.
aws lambda get-function-configuration \
--function-name YOUR_FUNCTION_NAME \
--query 'Role' \
--output text
Copy the returned ARN — every subsequent step targets this exact role.
Step 2: Check the Execution Role's Attached Policies for S3 Permissions
With the role ARN confirmed, list what policies are attached. IAM evaluates both managed policies and inline policies, so you need to check both. A common miss is adding s3:GetObject to an inline policy while a managed policy with an explicit Deny on S3 is still attached.
# List attached managed policies
aws iam list-attached-role-policies \
--role-name YOUR_ROLE_NAME
# List inline policies
aws iam list-role-policies \
--role-name YOUR_ROLE_NAME
If no S3 policy is present, add one. The minimum policy for reading a specific bucket is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
}
]
}
Apply it as an inline policy on the role:
aws iam put-role-policy \
--role-name YOUR_ROLE_NAME \
--policy-name LambdaS3ReadPolicy \
--policy-document file://s3-read-policy.json
Step 3: Inspect the S3 Bucket Policy for Explicit Denies or Missing Allows
Even with a correct IAM role policy, the bucket policy is evaluated independently. An explicit Deny in the bucket policy overrides any IAM Allow — this is the most common reason engineers are still getting a 403 after fixing the role. If the bucket has no policy at all and the bucket is in the same account as the Lambda, the IAM role policy alone is sufficient. If the bucket is in a different account, the bucket policy must explicitly allow the execution role.
aws s3api get-bucket-policy \
--bucket YOUR_BUCKET_NAME \
--query Policy \
--output text
Parse the output and look for any "Effect": "Deny" statement whose Principal or Condition could match your execution role ARN. Also check for aws:SourceVpc or aws:SourceVpce conditions that might exclude your Lambda's network path.
Step 4: Check for KMS Encryption on the Target Objects
This is where engineers lose the most time. The IAM role has s3:GetObject. The bucket policy is clean. The 403 persists. The object is encrypted with a customer-managed KMS key, and the execution role has no kms:Decrypt permission in the key policy. S3 returns a 403 — not a KMS-specific error — which makes this layer invisible until you look for it explicitly.
# Check the default encryption configuration of the bucket
aws s3api get-bucket-encryption \
--bucket YOUR_BUCKET_NAME
If the output shows aws:kms with a key ARN, retrieve the key policy and verify the execution role is listed as an allowed principal for kms:Decrypt:
aws kms get-key-policy \
--key-id YOUR_KMS_KEY_ID \
--policy-name default \
--output text
Add the execution role to the key policy's Allow statement for kms:Decrypt (and kms:GenerateDataKey if the function also writes). Key policy changes are applied through the KMS console or the put-key-policy API — the key policy must include the role ARN as a principal, not just an IAM policy attached to the role.
Step 5: Verify the VPC Endpoint Policy If Lambda Runs Inside a VPC
Lambda functions deployed inside a VPC route S3 traffic through a gateway VPC endpoint when one is configured. Gateway endpoints have their own resource policy, and a restrictive endpoint policy can block requests even when the IAM role and bucket policy are both correct. This layer is invisible from the S3 console and is frequently overlooked.
# Find the VPC endpoint for S3 in your VPC
aws ec2 describe-vpc-endpoints \
--filters Name=service-name,Values=com.amazonaws.us-east-1.s3 \
Name=vpc-id,Values=YOUR_VPC_ID \
--query 'VpcEndpoints[*].{ID:VpcEndpointId,Policy:PolicyDocument}' \
--output json
If the endpoint policy is not the AWS-managed full-access default, inspect it for principal or resource restrictions that exclude your execution role or bucket.
Step 6: Use IAM Policy Simulator to Validate the Effective Permission
After adjusting policies at one or more layers, use the IAM Policy Simulator to confirm the effective permission before redeploying. The simulator evaluates identity-based policies attached to the role but does not evaluate bucket policies or KMS key policies — treat a passing simulation as a necessary but not sufficient condition.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/YOUR_ROLE_NAME \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::YOUR_BUCKET_NAME/YOUR_OBJECT_KEY
A result of allowed confirms the IAM layer is clear. If it returns implicitDeny or explicitDeny, the role policy still needs correction before moving on to the other layers.
The Misdiagnosis That Costs the Most Time
Here is the pattern that shows up repeatedly in production incidents: the engineer adds s3:GetObject to the execution role, retests, and still gets a 403. The assumption is that the policy hasn't propagated yet, so they wait. They redeploy. They check the role again. Everything looks right.
What they missed: the bucket was created by a different team with a bucket policy that includes a blanket Deny on all principals except a specific CI/CD role ARN. The IAM role policy is irrelevant — the Deny in the bucket policy wins unconditionally. The fix is a single line in the bucket policy, but it takes 45 minutes to find because the engineer never looked at the bucket policy layer.
The diagnostic discipline is: treat every 403 as a multi-layer problem until each layer is explicitly cleared, not assumed clean.
Confirm Role ARN"] --> S2["Step 2
Check Role Policies"] S2 --> S3["Step 3
Inspect Bucket Policy"] S3 --> S4["Step 4
Check KMS Key Policy"] S4 --> S5["Step 5
Check VPC Endpoint Policy"] S5 --> S6["Step 6
IAM Policy Simulator"] S6 --> OK["✅ 403 Resolved"] style OK fill:#27ae60,color:#fff
Minimum IAM Policy Reference for Common S3 Operations
Use the table below to identify the exact IAM actions required for each operation. Apply only the actions your function actually needs — do not default to s3:*.
| Operation | Required IAM Action(s) | Resource Scope |
|---|---|---|
| Read an object | s3:GetObject | arn:aws:s3:::bucket/* |
| Write an object | s3:PutObject | arn:aws:s3:::bucket/* |
| Delete an object | s3:DeleteObject | arn:aws:s3:::bucket/* |
| List objects in a bucket | s3:ListBucket | arn:aws:s3:::bucket (bucket ARN, no trailing /*) |
| Read object metadata | s3:GetObject | arn:aws:s3:::bucket/* |
| Copy an object | s3:GetObject (source), s3:PutObject (destination) | Both bucket ARNs |
Note:s3:ListBucketapplies to the bucket ARN itself (arn:aws:s3:::bucket), not the object ARN with a wildcard. Applying it toarn:aws:s3:::bucket/*will not grant list access — this is a common policy authoring mistake that produces a 403 onListObjectsV2calls.
Wrap-Up: Resolving Lambda 403 Errors on S3
A Lambda 403 on S3 is never just a missing IAM action. Work through the layers in order: execution role policy, bucket policy, KMS key policy, and VPC endpoint policy. Clear each layer explicitly with the CLI commands above rather than assuming it is clean. The IAM Policy Simulator validates the identity layer only — use get-bucket-policy and get-key-policy to validate the resource layers independently.
For further reading, see the AWS IAM Policy Evaluation Logic documentation and the S3 Access Control Overview.
Glossary
| Term | Definition |
|---|---|
| Execution Role | The IAM role assumed by a Lambda function at runtime. Defines what AWS APIs the function is permitted to call. |
| Resource-Based Policy | A policy attached directly to an AWS resource (e.g., an S3 bucket policy) that specifies which principals can access it. |
| Explicit Deny | An IAM policy statement with "Effect": "Deny" that overrides any Allow statement in any other policy in the evaluation chain. |
| SSE-KMS | Server-Side Encryption using a KMS key. Requires the calling principal to have kms:Decrypt permission in the KMS key policy to read encrypted objects. |
| VPC Gateway Endpoint | A VPC endpoint type for S3 and DynamoDB that routes traffic within the AWS network. Has its own resource policy that can restrict access independently of IAM. |
Comments
Post a Comment