AWS
S3
Lambda
Cloud Computing
Access Policy

S3 Policy to Allow Lambda

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

For an AWS Lambda function to read from or write to an S3 bucket, two things must be configured: the Lambda function's execution role must have an IAM policy granting S3 permissions, and (optionally) the S3 bucket policy can explicitly allow the Lambda role. The execution role policy is the primary and required mechanism. A bucket policy is needed only if the bucket is in a different AWS account or has restrictive access controls.

Lambda Execution Role Policy

The Lambda execution role is an IAM role attached to the function. Add S3 permissions to this role:

json
1{
2    "Version": "2012-10-17",
3    "Statement": [
4        {
5            "Effect": "Allow",
6            "Action": [
7                "s3:GetObject",
8                "s3:PutObject",
9                "s3:DeleteObject"
10            ],
11            "Resource": "arn:aws:s3:::my-bucket/*"
12        },
13        {
14            "Effect": "Allow",
15            "Action": [
16                "s3:ListBucket"
17            ],
18            "Resource": "arn:aws:s3:::my-bucket"
19        }
20    ]
21}
  • s3:GetObject/PutObject/DeleteObject apply to objects (my-bucket/*)
  • s3:ListBucket applies to the bucket itself (my-bucket, no /*)

Creating the Role with AWS CLI

bash
1# Create the execution role
2aws iam create-role \
3  --role-name lambda-s3-role \
4  --assume-role-policy-document '{
5    "Version": "2012-10-17",
6    "Statement": [{
7      "Effect": "Allow",
8      "Principal": {"Service": "lambda.amazonaws.com"},
9      "Action": "sts:AssumeRole"
10    }]
11  }'
12
13# Attach S3 permissions
14aws iam put-role-policy \
15  --role-name lambda-s3-role \
16  --policy-name S3Access \
17  --policy-document file://s3-policy.json
18
19# Attach basic Lambda execution (CloudWatch Logs)
20aws iam attach-role-policy \
21  --role-name lambda-s3-role \
22  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

S3 Bucket Policy (Cross-Account or Restrictive Buckets)

If the S3 bucket is in a different account or has a restrictive bucket policy, add an explicit allow:

json
1{
2    "Version": "2012-10-17",
3    "Statement": [
4        {
5            "Effect": "Allow",
6            "Principal": {
7                "AWS": "arn:aws:iam::123456789012:role/lambda-s3-role"
8            },
9            "Action": [
10                "s3:GetObject",
11                "s3:PutObject"
12            ],
13            "Resource": "arn:aws:s3:::my-bucket/*"
14        }
15    ]
16}

Replace 123456789012 with the AWS account ID that owns the Lambda function.

CloudFormation / SAM Template

yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Transform: AWS::Serverless-2016-10-31
3
4Resources:
5  MyFunction:
6    Type: AWS::Serverless::Function
7    Properties:
8      Handler: index.handler
9      Runtime: python3.12
10      Policies:
11        - S3ReadPolicy:
12            BucketName: !Ref MyBucket
13        - S3CrudPolicy:
14            BucketName: !Ref MyBucket
15
16  MyBucket:
17    Type: AWS::S3::Bucket

SAM provides shorthand policy templates (S3ReadPolicy, S3CrudPolicy) that generate the correct IAM statements automatically.

Terraform Example

hcl
1resource "aws_iam_role" "lambda_role" {
2  name = "lambda-s3-role"
3
4  assume_role_policy = jsonencode({
5    Version = "2012-10-17"
6    Statement = [{
7      Action    = "sts:AssumeRole"
8      Effect    = "Allow"
9      Principal = { Service = "lambda.amazonaws.com" }
10    }]
11  })
12}
13
14resource "aws_iam_role_policy" "s3_access" {
15  name = "s3-access"
16  role = aws_iam_role.lambda_role.id
17
18  policy = jsonencode({
19    Version = "2012-10-17"
20    Statement = [
21      {
22        Effect   = "Allow"
23        Action   = ["s3:GetObject", "s3:PutObject"]
24        Resource = "${aws_s3_bucket.my_bucket.arn}/*"
25      },
26      {
27        Effect   = "Allow"
28        Action   = ["s3:ListBucket"]
29        Resource = aws_s3_bucket.my_bucket.arn
30      }
31    ]
32  })
33}
34
35resource "aws_lambda_function" "my_function" {
36  function_name = "my-function"
37  role          = aws_iam_role.lambda_role.arn
38  handler       = "index.handler"
39  runtime       = "python3.12"
40  filename      = "lambda.zip"
41}

S3 Event Trigger (Lambda Invocation from S3)

To trigger Lambda when an object is uploaded to S3, you also need a resource-based policy on the Lambda function:

bash
1# Allow S3 to invoke the Lambda function
2aws lambda add-permission \
3  --function-name my-function \
4  --statement-id s3-trigger \
5  --action lambda:InvokeFunction \
6  --principal s3.amazonaws.com \
7  --source-arn arn:aws:s3:::my-bucket \
8  --source-account 123456789012

Then configure the S3 event notification:

bash
1aws s3api put-bucket-notification-configuration \
2  --bucket my-bucket \
3  --notification-configuration '{
4    "LambdaFunctionConfigurations": [{
5      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
6      "Events": ["s3:ObjectCreated:*"]
7    }]
8  }'

Lambda Function Example

python
1import boto3
2import json
3
4s3 = boto3.client("s3")
5
6def handler(event, context):
7    # Triggered by S3 event
8    bucket = event["Records"][0]["s3"]["bucket"]["name"]
9    key = event["Records"][0]["s3"]["object"]["key"]
10
11    # Read the object
12    response = s3.get_object(Bucket=bucket, Key=key)
13    content = response["Body"].read().decode("utf-8")
14
15    # Write a processed result
16    s3.put_object(
17        Bucket=bucket,
18        Key=f"processed/{key}",
19        Body=json.dumps({"original_size": len(content)})
20    )
21
22    return {"statusCode": 200}

Common Pitfalls

  • Using s3:* as the action: This grants full S3 access including deleting buckets and modifying policies. Always use the minimum actions needed (GetObject, PutObject, ListBucket). Follow the principle of least privilege.
  • Wrong Resource ARN format: s3:ListBucket requires the bucket ARN (arn:aws:s3:::my-bucket), while s3:GetObject requires the object ARN (arn:aws:s3:::my-bucket/*). Mixing these up causes AccessDenied errors that are hard to diagnose.
  • Forgetting the Lambda invocation permission for S3 triggers: The execution role controls what Lambda can access. A separate resource-based policy (lambda:InvokeFunction) controls what can trigger Lambda. S3 needs both.
  • Cross-account buckets without a bucket policy: The execution role alone is not sufficient for cross-account access. The target bucket must have a bucket policy that explicitly allows the Lambda role from the other account.
  • Not including CloudWatch Logs permissions: Without AWSLambdaBasicExecutionRole (or equivalent), Lambda cannot write logs. Debugging permission issues becomes impossible without logs.

Summary

  • Attach S3 permissions to the Lambda execution role — this is the primary access mechanism
  • Use specific actions (s3:GetObject, s3:PutObject) instead of s3:*
  • s3:ListBucket targets the bucket ARN; object actions target bucket/*
  • Add a bucket policy only for cross-account access or restrictive bucket configurations
  • For S3-triggered Lambda, add both the execution role policy and a resource-based invocation permission
  • Use SAM/CloudFormation policy templates (S3ReadPolicy, S3CrudPolicy) for managed, least-privilege policies

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.