Lambda 403 on S3: Diagnosing & Fixing Execution Role Permissions
Your Lambda function executes cleanly — no runtime crashes, no timeouts — yet every S3 API call returns a 403 Forbidden. This is one of the most common IAM misconfigurations in serverless architectures, and the root cause is almost always a missing or misconfigured permission on the Lambda execution role.
TL;DR
| Symptom | Root Cause | Fix |
|---|---|---|
AccessDenied / 403 on S3 GetObject | Execution role lacks s3:GetObject permission | Attach inline or managed policy granting s3:GetObject on the target bucket/prefix |
| 403 even with correct IAM policy | S3 bucket policy explicitly denies the role | Remove or adjust the bucket policy Deny statement |
| 403 on a KMS-encrypted object | Role lacks kms:Decrypt on the CMK | Grant kms:Decrypt to the execution role in the KMS key policy |
| 403 on a cross-account bucket | Both the role policy AND bucket policy must grant access | Update both the execution role policy and the bucket resource policy |
How Lambda Authenticates to S3
When a Lambda function calls the S3 API, it uses temporary credentials vended by AWS STS and scoped to its execution role. S3 then evaluates two independent policy layers before allowing or denying the request:
(Temp Credentials)"] ER["Execution Role
(Identity-Based Policy)"] S3["S3 Service"] BP["Bucket Policy
(Resource-Based Policy)"] KMS["KMS Key Policy
(if CMK encrypted)"] OBJ["S3 Object"] LF -->|"Assumes"| ER ER -->|"Vends credentials via"| STS STS -->|"Signed API Request"| S3 S3 -->|"Evaluates"| BP S3 -->|"Evaluates"| KMS BP -->|"Allow + no Deny"| OBJ KMS -->|"kms:Decrypt allowed"| OBJ style LF fill:#FF9900,color:#fff style ER fill:#DD344C,color:#fff style S3 fill:#3F8624,color:#fff style BP fill:#1A6496,color:#fff style KMS fill:#7B4F9E,color:#fff style OBJ fill:#3F8624,color:#fff
- Lambda Execution Role — The IAM identity policy attached to the role. This is the identity-based policy that says what the role is allowed to do.
- S3 Bucket Policy — The resource-based policy attached to the bucket. An explicit
Denyhere overrides any identity-basedAllow, regardless of the role's permissions. - S3 Access Control Lists (ACLs) — Legacy mechanism; relevant only if ACLs are enabled on the bucket (disabled by default for new buckets).
- KMS Key Policy — If the object is encrypted with a customer-managed key (CMK), the execution role must also be a principal in the KMS key policy with
kms:Decrypt.
Analogy: Think of the execution role policy as your employee badge (proves who you are and what you're allowed to access company-wide), and the S3 bucket policy as a room-level access list. Even if your badge grants building access, a locked room with your name on the deny list will still turn you away.
Step-by-Step Diagnosis
for exact action name"] ROLE["Check Execution Role
in IAM Console"] ROLE_OK{"s3:GetObject on
correct ARN present?"} BP["Check S3 Bucket Policy
for Deny statements"] BP_OK{"Explicit Deny
present?"} CROSS{"Cross-account
bucket?"} CROSS_BP["Add execution role ARN
to bucket policy Allow"] KMS{"Object encrypted
with CMK?"} KMS_FIX["Add kms:Decrypt to
KMS Key Policy"] FIX_ROLE["Add s3:GetObject on
arn:aws:s3:::bucket/*
to execution role"] REMOVE_DENY["Remove or scope
the Deny statement"] RESOLVED(["403 Resolved ✅"]) START --> CW CW --> ROLE ROLE --> ROLE_OK ROLE_OK -->|"No"| FIX_ROLE FIX_ROLE --> RESOLVED ROLE_OK -->|"Yes"| BP BP --> BP_OK BP_OK -->|"Yes"| REMOVE_DENY REMOVE_DENY --> RESOLVED BP_OK -->|"No"| CROSS CROSS -->|"Yes"| CROSS_BP CROSS_BP --> RESOLVED CROSS -->|"No"| KMS KMS -->|"Yes"| KMS_FIX KMS_FIX --> RESOLVED KMS -->|"No"| RESOLVED style START fill:#DD344C,color:#fff style RESOLVED fill:#3F8624,color:#fff style FIX_ROLE fill:#FF9900,color:#fff style CROSS_BP fill:#FF9900,color:#fff style KMS_FIX fill:#FF9900,color:#fff style REMOVE_DENY fill:#FF9900,color:#fff
Step 1 — Identify the Exact Error
The Lambda function's CloudWatch Logs will contain the full error. Look for the AccessDenied error code and the specific S3 action that was denied:
An error occurred (AccessDenied) when calling the GetObject operation:
Access Denied
The action name (GetObject, PutObject, ListBucket, etc.) tells you exactly which IAM action to grant.
Step 2 — Check the Execution Role
Navigate to IAM → Roles → [Your Lambda Execution Role] → Permissions. Verify that a policy exists granting the required S3 action on the correct resource ARN. A common mistake is granting permissions on the bucket ARN (arn:aws:s3:::my-bucket) but forgetting the object-level ARN (arn:aws:s3:::my-bucket/*).
Step 3 — Check the S3 Bucket Policy
Navigate to S3 → [Your Bucket] → Permissions → Bucket Policy. Look for any "Effect": "Deny" statements that might match your execution role's ARN or use wildcard principals.
Step 4 — Check S3 Block Public Access (if applicable)
If your bucket policy was intended to grant cross-account access, ensure that Block Public Access settings are not inadvertently blocking the bucket policy from taking effect for cross-account principals.
Step 5 — Check KMS Permissions (if applicable)
If the S3 objects are encrypted with an AWS KMS customer-managed key, the execution role must be explicitly listed as a principal in the KMS key policy with the kms:Decrypt (and optionally kms:GenerateDataKey for writes) action.
The Fix: Correct IAM Policy for the Execution Role
Attach the following least-privilege inline policy to your Lambda execution role. Replace my-bucket and the prefix as appropriate.
🔽 [Click to expand] — IAM Policy: S3 Read Access for Lambda Execution Role
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3GetObject",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-bucket/my-prefix/*"
},
{
"Sid": "AllowS3ListBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["my-prefix/*"]
}
}
}
]
}
Critical ARN distinction:
arn:aws:s3:::my-bucket— Refers to the bucket itself. Required for actions likes3:ListBucket.arn:aws:s3:::my-bucket/*— Refers to objects inside the bucket. Required for actions likes3:GetObject,s3:PutObject,s3:DeleteObject.
Attaching the Policy via AWS CLI
🔽 [Click to expand] — AWS CLI: Attach Inline Policy to Execution Role
# Save the policy to a file first
cat > s3-read-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3GetObject",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/my-prefix/*"
},
{
"Sid": "AllowS3ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["my-prefix/*"]
}
}
}
]
}
EOF
# Attach the inline policy to the execution role
aws iam put-role-policy \
--role-name MyLambdaExecutionRole \
--policy-name S3ReadAccessPolicy \
--policy-document file://s3-read-policy.json
Cross-Account Bucket Access
If the S3 bucket lives in a different AWS account, granting permissions on the execution role alone is insufficient. You must also update the bucket policy in the target account to explicitly allow the execution role's ARN.
🔽 [Click to expand] — S3 Bucket Policy: Grant Cross-Account Lambda Access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCrossAccountLambdaRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyLambdaExecutionRole"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
KMS-Encrypted Objects
If objects are encrypted with a KMS customer-managed key, add the following to the KMS key policy (not the IAM role policy) in addition to the S3 permissions above:
{
"Sid": "AllowLambdaDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyLambdaExecutionRole"
},
"Action": [
"kms:Decrypt"
],
"Resource": "*"
}
Note: The "Resource": "*" in a KMS key policy refers only to the key itself, not all KMS keys in the account. This is standard KMS key policy syntax.
Using IAM Policy Simulator to Verify
Before deploying, use the IAM Policy Simulator to validate that your execution role is actually allowed to perform the intended S3 action on the target resource. Navigate to IAM → Policy Simulator, select your execution role, choose the S3 service, select the action (e.g., GetObject), and specify the resource ARN.
Glossary
| Term | Definition |
|---|---|
| Execution Role | The IAM role assumed by a Lambda function at runtime. Defines what AWS services and resources the function can interact with. |
| Identity-Based Policy | An IAM policy attached to an IAM principal (user, group, or role) that specifies what actions that principal can perform. |
| Resource-Based Policy | A policy attached directly to an AWS resource (e.g., an S3 bucket policy) that specifies which principals can access that resource. |
| Explicit Deny | An IAM policy statement with "Effect": "Deny". Always overrides any Allow statement, regardless of policy type or order of evaluation. |
| CMK (Customer-Managed Key) | A KMS encryption key created and managed by the customer, as opposed to an AWS-managed key. Requires explicit key policy grants for cross-service access. |
Next Steps
- 📖 AWS Docs: Lambda Execution Role
- 📖 AWS Docs: S3 Access Control Overview
- 📖 AWS Docs: IAM Policy Simulator
- 🔒 Always scope S3 resource ARNs to the minimum required prefix — avoid
arn:aws:s3:::*in production execution roles.
Related Posts
- 📄 S3 Access Denied Despite Public Object: How Block Public Access Overrides Object ACLs
- 📄 AWS IAM Policy Structure: Decoding Effect, Action, Resource, and Condition
- 📄 IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User
- 📄 IAM Groups vs. Direct Policy Attachment: Why Groups Always Win
Comments
Post a Comment