Files
lambda_local_runner/docs/lambdas-md/lambda-07-iam.md
2026-05-11 20:13:11 -03:00

3.3 KiB

IAM & Permissions

Execution role vs resource policy. The two policies most people confuse.

Two independent permission layers

Lambda has two separate permission surfaces that must each be correct independently. Confusing them is the most common "it works locally but not in AWS" failure.

Layer Question it answers Who creates it
Execution role What can this Lambda function do once running? (call S3, write to DynamoDB, publish to SNS…) You — attached at function creation
Resource policy Who is allowed to invoke this Lambda function? (API Gateway, another account, EventBridge…) AWS adds it automatically for most triggers; you add it for cross-account or manual grants

Execution role

The execution role is an IAM role that Lambda assumes when running your function. Every Lambda must have one. The role's attached policies determine what AWS API calls the function can make. At minimum, every function needs:

# minimum: write its own logs
logs:CreateLogGroup
logs:CreateLogStream
logs:PutLogEvents

Common additions for a function that reads/writes S3:

s3:GetObject
s3:PutObject
s3:ListBucket        # needed for paginator; often forgotten
kms:Decrypt          # if the bucket uses a CMK, this is also required

The AWSLambdaBasicExecutionRole managed policy covers logs only — it is intentionally minimal. AWSLambdaVPCAccessExecutionRole adds the ENI permissions needed when the function is in a VPC.

Resource policy

The resource policy is attached to the Lambda function itself (not an IAM identity). When you add an S3 event notification or API Gateway integration in the console, AWS automatically adds a resource policy entry allowing that service to invoke the function. For cross-account invocations you add this manually via aws lambda add-permission.

# grant another account permission to invoke
aws lambda add-permission \
  --function-name my-function \
  --principal 123456789012 \  # the other AWS account
  --action lambda:InvokeFunction \
  --statement-id cross-account-invoke

Common mistakes

  • Missing s3:ListBucket on the bucket resource. ListObjectsV2 requires this on the bucket ARN (not the object ARN). Forgetting it causes AccessDenied on the paginator even when GetObject works fine.
  • Wrong resource ARN scope. s3:GetObject must be on arn:aws:s3:::bucket-name/*; s3:ListBucket must be on arn:aws:s3:::bucket-name. Swapping them is a frequent typo.
  • CMK not in execution role. KMS-encrypted bucket objects require both s3:GetObject and kms:Decrypt. The KMS key policy must also allow the role. Two separate policy documents, two separate denial points.
  • No resource policy for new trigger. If you wire up EventBridge manually (not via the console), the trigger silently fails because there's no resource policy entry granting EventBridge lambda:InvokeFunction.

Diagnosing permission errors

CloudTrail is the ground truth. Filter by errorCode: "AccessDenied" and userIdentity.arn matching the execution role ARN. The event tells you exactly which action on which resource was denied. CloudWatch will show the error in the Lambda log if you let the exception propagate, but CloudTrail shows it even when the call is made from a library that swallows the error.