Lambda Execution Roles
When your AWS Lambda function (Amazon’s “run code without managing servers” service) needs to read from a database, write to a bucket, or even just print a log line, it does not use your personal credentials. Instead it assumes an execution role — an IAM role (an AWS identity with a set of permissions that another service can borrow) that says exactly what the function is allowed to do. Getting this role right is the difference between a function that works, a function that throws confusing “access denied” errors, and a function that is dangerously over-permissioned. This page explains the role clearly and walks through building a tight, least-privilege one.
Execution role vs resource policy: two different doors
This is the single most important idea on the page, and the source of most Lambda permission confusion. Permissions flow in two directions, and they are controlled by two completely separate things.
| Concept | Direction | Question it answers | Where it lives |
|---|---|---|---|
| Execution role | Outbound | ”What is my function allowed to do to other AWS services?” | IAM role attached to the function |
| Resource / invoke policy | Inbound | ”Who is allowed to trigger this function?” | A resource-based policy on the function itself |
Think of it like a building. The execution role is the keycard your function carries — it decides which rooms (S3, DynamoDB, CloudWatch) the function can walk into. The resource policy is the lock on your own front door — it decides which outsiders (API Gateway, an S3 bucket event, EventBridge) are allowed to ring the bell and wake your function up.
Gotcha: These two are constantly mixed up. If API Gateway returns a
403/“not authorized” when calling your function, the fix is usually the resource policy (the inbound permission), not the execution role. If your function code itself getsAccessDeniedwhile calling, say, DynamoDB, the fix is the execution role. Both matter, and editing the wrong one is the classic way to lose an afternoon.
We focus mostly on the execution role here, but always keep the resource policy in the back of your mind.
What every execution role needs: CloudWatch Logs
The second most common “access denied” surprise is logging. By default, anything your function prints (a console.log, print, or an unhandled error) is sent to Amazon CloudWatch Logs (AWS’s central logging service). But Lambda can only write those logs if its execution role allows it.
If you create a role by hand and forget these three actions, your function may run fine but you will see nothing in the logs — no errors, no output — which makes debugging miserable.
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:111122223333:*"
}
AWS provides a managed policy named AWSLambdaBasicExecutionRole that contains exactly these logging permissions. When you create a function in the console, this is attached automatically. When you build a role yourself, attach it first thing.
Building a least-privilege execution role
Least privilege means granting only the exact permissions the function uses and nothing more. If your function reads one DynamoDB table and writes to one S3 bucket, the role should allow those two things on those two resources — not dynamodb:* on *.
When to use this (and when not to)
- Use a dedicated, scoped role per function. This is the default best practice. One function, one role, narrowly scoped.
- Do NOT reuse a single broad “lambda-can-do-everything” role across many functions. A bug or compromise in one function then exposes everything.
- Do NOT attach
AdministratorAccessorPowerUserAccessto a function “to make the error go away.” That is the most common security mistake in serverless projects.
Step 1 — create the role with a trust policy
A role needs a trust policy that names who can assume it. For Lambda, the trusted principal (the identity allowed to use the role) is the Lambda service itself.
Save this as trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
AWS CLI (v2):
aws iam create-role \
--role-name order-processor-role \
--assume-role-policy-document file://trust-policy.json
Output:
{
"Role": {
"RoleName": "order-processor-role",
"Arn": "arn:aws:iam::111122223333:role/order-processor-role",
"CreateDate": "2026-06-15T14:08:22+00:00"
}
}
Step 2 — attach logging permissions
aws iam attach-role-policy \
--role-name order-processor-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
This single managed policy covers CloudWatch Logs, so you never hit the “blank logs” problem.
Step 3 — add a scoped inline policy for the services it touches
Suppose the function reads from a DynamoDB table Orders and writes objects to the bucket order-receipts. Save this as app-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOrdersTable",
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:us-east-1:111122223333:table/Orders"
},
{
"Sid": "WriteReceipts",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::order-receipts/*"
}
]
}
aws iam put-role-policy \
--role-name order-processor-role \
--policy-name order-processor-access \
--policy-document file://app-policy.json
Notice each statement names specific actions on specific resources. That is least privilege in practice.
Console steps (the equivalent)
- Open the IAM console and choose Roles, then Create role.
- For trusted entity type, pick AWS service, then choose Lambda as the use case. Click Next.
- Search for and select AWSLambdaBasicExecutionRole (the logging permissions). Click Next.
- Name the role (e.g.
order-processor-role) and click Create role. - Open the new role, choose Add permissions → Create inline policy, paste the JSON from Step 3, and save it.
- In the Lambda console, open your function, go to Configuration → Permissions, click Edit, and select this role under Execution role.
Defining the role in infrastructure-as-code
In real projects you define the role alongside the function so it is version-controlled and repeatable. Here it is with AWS SAM (Serverless Application Model):
Resources:
OrderProcessor:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs20.x
Policies:
- AWSLambdaBasicExecutionRole
- Statement:
- Effect: Allow
Action: [dynamodb:GetItem, dynamodb:Query]
Resource: arn:aws:dynamodb:us-east-1:111122223333:table/Orders
- Effect: Allow
Action: s3:PutObject
Resource: arn:aws:s3:::order-receipts/*
SAM creates a dedicated, scoped role automatically from this Policies block — you rarely need to hand-write the trust policy when using a framework.
The other door: granting invoke permission
For completeness, here is the inbound side. To let API Gateway (an AWS service for publishing APIs) trigger the function, you add a resource policy with add-permission — this does not touch the execution role at all:
aws lambda add-permission \
--function-name order-processor \
--statement-id apigw-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:111122223333:a1b2c3d4e5/*/POST/orders"
Tip: There is no extra cost for IAM roles, policies, or resource policies — they are free. The only “cost” of getting permissions wrong is debugging time and security risk, so it is always worth scoping them tightly from the start.
Best Practices
- Give each function its own dedicated role; never share one broad role across many functions.
- Always include CloudWatch Logs permissions (use
AWSLambdaBasicExecutionRole) or you will debug blind. - Scope every statement to specific actions and specific resource ARNs — avoid
*on action or resource. - Never attach
AdministratorAccessto a function to silence an error; find the one missing permission instead. - Remember the two doors: execution role = what the function can call; resource policy = who can invoke the function.
- Define roles in IaC (SAM, CloudFormation, or Terraform) so they are reviewed, versioned, and reproducible.
- Use IAM Access Analyzer and CloudTrail to spot unused permissions and tighten roles over time.