# 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`. ```bash # 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.