---
cuppy_hub: aws
cuppy_path: reference/lambda/permissions.md
cuppy_section: docs
cuppy_doc: EE5Q-7WR9
cuppy_doc_uuid: e402ca8b-58e3-4d16-8a61-b3f70d0d9dae
cuppy_permalink: https://cuppy.io/aws/d/EE5Q-7WR9
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/reference/lambda/permissions
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 5d593eb89a6c95e17cd56a8769ad8f26fb07bc0f73a274b9ab6abe4d5e861b9c
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Lambda's two policies

**The execution role says what the function can do. The resource-based policy says who can invoke or manage the function.** They are separate documents, attached in different places, and a missing permission on either one produces a different error.

| | Execution role | Resource-based (function) policy |
|---|---|---|
| Attached to | An IAM role, set as the function's `Role` | The function, a version or an alias |
| Controls | The function's calls to AWS: S3, DynamoDB, logs, KMS | Callers of `lambda:InvokeFunction` and other Lambda actions |
| Principal | `lambda.amazonaws.com` in the role's trust policy | Services (`s3.amazonaws.com`, `events.amazonaws.com`, …), accounts, organizations |
| Missing permission looks like | `AccessDenied…` / `…not authorized to perform: s3:GetObject` inside the function's logs | `…not authorized to perform: lambda:InvokeFunction on resource…` for the caller, or `Unable to validate the following destination configurations` when S3 saves a notification |
| Inspect | `aws iam list-attached-role-policies --role-name <role>` | `aws lambda get-policy --function-name <fn>` |

## Execution role

- The trust policy allows `lambda.amazonaws.com` to `sts:AssumeRole`. Lambda assumes it on every invocation and hands the code temporary credentials through environment variables, which the SDK picks up. **Don't call `sts:AssumeRole` on your own execution role**. If you really need the role to assume itself, it must trust itself explicitly.
- AWS managed policies to start from:
  - `AWSLambdaBasicExecutionRole`: CloudWatch Logs.
  - `AWSLambdaVPCAccessExecutionRole`: creating and deleting the network interfaces a VPC function needs. It does not grant network access; that comes from routes (`patterns/lambda-in-vpc.md`).
  - Service-specific ones, such as `AWSLambdaDynamoDBExecutionRole` for stream event sources.
- Trim before production. IAM Access Analyzer can generate a policy from the CloudTrail activity of the role over a date range.
- The console creates a role with basic permissions by default, or attaches one via IAM role manager when that is enabled in the account.
- Whoever creates or updates the function needs `iam:PassRole` on this role (`guides/iam/create-service-role.md`).

## Resource-based policy

### Before and after August 2026

Until 2026-08-25, the function policy could only be built **one grant at a time** with `add-permission`: one principal and one action per statement. Since then, Lambda supports **full IAM resource-based policies** in all commercial Regions. A single document can name several principals and actions and use any IAM condition key, such as source IP or principal tags. You can write it in the console JSON editor, the CLI, the SDKs, CloudFormation or SAM.

### The overwrite trap

`PutResourcePolicy` **replaces the entire policy** on the function, version or alias. Every statement added earlier with `add-permission`, including the ones the console added for triggers, is gone unless your document repeats it. Before you put a policy:

1. `aws lambda get-policy --function-name <fn>`: copy the existing statements and the `RevisionId`.
2. Merge your change into that document.
3. Put it with the `RevisionId`. If someone changed the policy in between, the call fails with `PreconditionFailedException` (412) instead of silently overwriting their change. The API page names both `GetResourcePolicy` and `GetFunction`/`GetAlias` as the place to read the current revision; it contradicts itself.

```ts
import { LambdaClient, PutResourcePolicyCommand } from "@aws-sdk/client-lambda";

const lambda = new LambdaClient({ region: "<region>" });
const fn = "arn:aws:lambda:<region>:<account>:function:<name>";

await lambda.send(new PutResourcePolicyCommand({
  ResourceArn: fn,
  // Replaces the WHOLE policy: include every grant you want to keep.
  Policy: JSON.stringify({
    Version: "2012-10-17",
    Statement: [{
      Sid: "S3AndEventBridgeInvoke",
      Effect: "Allow",
      Principal: { Service: ["s3.amazonaws.com", "events.amazonaws.com"] },
      Action: "lambda:InvokeFunction",
      Resource: fn,
      Condition: { StringEquals: { "aws:SourceAccount": "<account>" } },
    }],
  }),
  // RevisionId: "<current revision>", // fail with 412 instead of overwriting a concurrent change
}));
```

`ResourceArn` takes the function ARN, with or without a version or alias qualifier, and no wildcards. The policy is limited to 20,480 characters. A policy that would make the function public is rejected with `PublicPolicyException`. The CLI form is `aws lambda put-resource-policy --resource-arn … --policy file://policy.json` (UNVERIFIED command spelling; derived from the API name). **Calling it needs three IAM actions:** `lambda:PutResourcePolicy` plus its dependent actions `lambda:AddPermission` and `lambda:RemovePermission` (Service Authorization Reference). Likewise `lambda:GetResourcePolicy` depends on `lambda:GetPolicy`, and `lambda:DeleteResourcePolicy` on `lambda:RemovePermission`. A deploy role that only had `lambda:AddPermission` before August 2026 fails with AccessDenied on the new call.

**Use one style per function.** Either manage the whole document with `put-resource-policy` (or IaC), or add grants with `add-permission`. Mixing them is how grants disappear.

### `add-permission` (still the per-grant form)

```
aws lambda add-permission --function-name <fn> --statement-id <unique-id> \
  --action lambda:InvokeFunction --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::<bucket> --source-account <bucket-owner-account>
```

A duplicate `--statement-id` fails with `ResourceConflictException`. Remove a grant with `aws lambda remove-permission --function-name <fn> --statement-id <id>`.

### Scoping service principals

When a service such as S3, SNS or EventBridge invokes the function, condition the grant on **`aws:SourceArn`** (the specific bucket, topic or rule) and **`aws:SourceAccount`**. Without them, any resource of that service in any account that knows the function ARN can invoke it (the confused-deputy problem: `patterns/iam/trust-policies.md`).

### Cross-account

Account B invoking a function in account A needs both: B's identity policy allows `lambda:InvokeFunction` on the function ARN, **and** A's function policy allows B (an account principal, a role ARN, or `aws:PrincipalOrgID` for an organization).

## See also

- `patterns/s3-to-lambda.md`: the most common grant.
- `reference/lambda/errors.md`: `PolicyLengthExceededException`, `PublicPolicyException`, `PreconditionFailedException`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
