Compute
Lambda execution role: trust and logging permissions
An execution role that only the Lambda service can assume and that can write only to its own function's log group, with checks that change nothing.
Published · Updated .
The problem
Every Lambda function runs with an execution role. Lambda assumes the role when the function is invoked and hands the temporary credentials to your code. The role has two halves that fail in different ways: the trust policy decides who can assume it, and the permissions policies decide what those credentials can do. A common shortcut is one shared role with the AWS managed policy AWSLambdaBasicExecutionRole, whose single statement allows logs:CreateLogGroup, logs:CreateLogStream and logs:PutLogEvents on every resource. Any function using that role can write into any log group in the account.
This example builds a role for one function, order-processor in eu-west-1, that only Lambda can assume and that can write only to /aws/lambda/order-processor. You create the log group yourself, with a retention period, so the role does not need logs:CreateLogGroup.
The policies
Trust policy: only the Lambda service can assume the role
IAM role trust policy. Attach to: The execution role's trust relationship (the AssumeRolePolicyDocument passed to create-role)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LambdaServiceAssumesRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}The form generators on this site don't edit trust policy documents; copy it, then check it with IAM Access Analyzer as shown below.
Statement by statement
- LambdaServiceAssumesRole: only the Lambda service principal may call sts:AssumeRole on this role. A trust policy has no Resource element because the role is the resource. There is deliberately no aws:SourceAccount or aws:SourceArn condition. Lambda's documentation shows the execution-role trust policy with the service principal alone, and says aws:SourceArn applies to policies where the function is the target, not to the role Lambda assumes. Which functions use the role is controlled elsewhere: attaching it to a function requires iam:PassRole on it.
Permissions policy: write only this function's log streams
IAM identity-based policy. Attach to: The execution role, as an inline or customer managed policy. Nothing else on the role should grant logs actions on "*".
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteOwnLogStreams",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/order-processor:*"
}
]
}Statement by statement
- WriteOwnLogStreams: the Service Authorization Reference lists log-stream as the resource type for both CreateLogStream and PutLogEvents, and a stream ARN has the form log-group:<group>:log-stream:<stream>. The trailing :* therefore matches every stream inside /aws/lambda/order-processor. Log group names cannot contain a colon, so the pattern cannot reach another group. Writing order-processor* without the colon would also match order-processor-v2 or order-processor-admin.
Before you start
- Permission in account 111122223333 to create roles and inline policies, and iam:PassRole on the new role so you can attach it to the function.
- A function named order-processor in eu-west-1, or a test function whose invocation has no side effects.
- AWS CLI v2. The commands read the two documents from trust-policy.json and policy.json in the current directory.
Deploy it step by step
- Validate both documents. Access Analyzer checks each document against the grammar for its policy type. It cannot tell you whether Lambda will assume the role or whether log writes succeed; the checks below answer that.
Commands (replace <placeholders>) aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::IAM::AssumeRolePolicyDocument --policy-document file://trust-policy.json aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json - Create the log group yourself and set retention. By default /aws/lambda/<function name> is created the first time the function runs, which needs logs:CreateLogGroup on the role, and the new group keeps events indefinitely. Creating it up front, ideally in the same stack as the function, sets retention and encryption and removes the need for CreateLogGroup. If the group may not exist yet in some Region, grant CreateLogGroup on this one group's ARN, not on every group.
Commands (replace <placeholders>) aws logs create-log-group --log-group-name /aws/lambda/order-processor --region eu-west-1 aws logs put-retention-policy --log-group-name /aws/lambda/order-processor --retention-in-days 30 --region eu-west-1 - Create one role for this function and add the policy. A role per function keeps a compromise of one function away from another's resources. If the function was created in the console, check for AWSLambdaBasicExecutionRole or a generated policy on its old role; IAM allows a request when any attached policy allows it, so a leftover broad policy cancels this one.
Commands (replace <placeholders>) aws iam create-role --role-name order-processor-exec --assume-role-policy-document file://trust-policy.json aws iam put-role-policy --role-name order-processor-exec --policy-name write-own-logs --policy-document file://policy.json - Attach the role and check where the function logs. Lambda's advanced logging controls (the LoggingConfig setting) choose text or JSON format, log levels, and optionally a custom log group in place of /aws/lambda/<function name>. If LoggingConfig.LogGroup names a custom group, change the policy's Resource to that group in the same change. Lambda does not add permissions when you change the group through the CLI. Custom group names must not start with aws/. Several functions can share a custom group, and Lambda then puts the function name and version in each stream name. If update-function-configuration says the role cannot be assumed right after you created it, wait a few seconds and retry, because IAM changes take a moment to propagate.
Commands (replace <placeholders>) aws lambda update-function-configuration --function-name order-processor --role arn:aws:iam::111122223333:role/order-processor-exec --region eu-west-1 aws lambda get-function-configuration --function-name order-processor --query LoggingConfig --region eu-west-1
Verify it without changing anything
These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.
Invoke a test function and read what it wrote
Invoke only a function or test event that has no side effects. The second command runs with your own credentials and shows whether the function managed to write. Use a start time, in epoch milliseconds, from just before the invocation.
aws lambda invoke --function-name order-processor --payload '{}' --cli-binary-format raw-in-base64-out --region eu-west-1 response.json
aws logs filter-log-events --log-group-name /aws/lambda/order-processor --start-time <epoch-milliseconds> --region eu-west-1Expected: invoke should report StatusCode 200; a FunctionError field means your code failed, not the role. filter-log-events should return new events from a recent stream, such as START, END and REPORT lines with the text format, or the equivalent platform records with JSON. If the invocation succeeds but no events arrive after a minute or so, the role probably cannot write. Check the :* suffix and the LoggingConfig log group.
Simulate the policy against this group and a neighbor
simulate-custom-policy evaluates only the document you pass in. It is the quickest way to confirm the resource pattern before you deploy.
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names logs:CreateLogStream logs:PutLogEvents --resource-arns arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/order-processor:log-stream:test arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/payments-api:log-stream:testExpected: EvalDecision should be allowed for both actions on the order-processor stream and implicitDeny on the payments-api stream. An implicitDeny on order-processor usually means the Resource lost its :* suffix.
Check every policy on the role, not just yours
simulate-principal-policy evaluates all identity policies attached to the role, so it catches a leftover managed policy that the custom simulation cannot see.
aws iam list-attached-role-policies --role-name order-processor-exec
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::111122223333:role/order-processor-exec --action-names logs:PutLogEvents --resource-arns arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/payments-api:log-stream:test
aws iam get-role --role-name order-processor-exec --query Role.AssumeRolePolicyDocumentExpected: The attached list should be empty or hold only policies you expect. The simulation should return implicitDeny for another function's group; allowed usually means a broad policy such as AWSLambdaBasicExecutionRole is still attached. get-role should show one statement for lambda.amazonaws.com and no other principal.
Common mistakes and troubleshooting
Resource ends at the group name
Without :* (or :log-stream:*), CreateLogStream and PutLogEvents are denied because they authorize against stream ARNs. The function typically still runs, so the symptom is an empty log group rather than a failed invocation.
The console quietly broadens the role
When you change the log group in the console with "Add required permissions" selected, Lambda can add PutLogEvents for any log group to the role. Clear that option, or review the role afterwards.
"The role defined for the function cannot be assumed by Lambda"
This error can mean the role was used seconds after creation, or that the trust policy names the wrong principal, for example a typo in lambda.amazonaws.com. Lambda@Edge functions also need edgelambda.amazonaws.com in the trust policy.
Limitations and production considerations
- Every library in the function can use the role's credentials. This policy limits where that code can write logs, not what it writes: compromised code can still flood or forge entries in its own group.
- Logging is only the baseline. Add the function's business permissions as separate, equally narrow statements. For sensitive ones, consider the lambda:SourceFunctionArn condition key, which Lambda adds for calls made from the execution environment and for the log calls it makes on your behalf. It works in identity policies and SCPs, not in resource-based policies, and must be the unqualified function ARN.
- Retention, encryption and who can read the group are separate decisions. If you send function logs to Amazon S3 or Firehose instead of CloudWatch Logs, the permissions differ and Lambda does not add them for you.
Official AWS documentation
- Defining Lambda function permissions with an execution role
- Configuring CloudWatch log groups (AWS Lambda)
- Using source function ARN to control function access behavior
- AWSLambdaBasicExecutionRole (AWS managed policy)
- Grant a user permissions to pass a role to an AWS service
- Actions, resources, and condition keys for Amazon CloudWatch Logs
Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).