AWS Lambda Security Model Explained: Roles, Resource Policies, VPCs & Secrets
The Lambda security model has four distinct layers that work together: the execution role that controls what the function can do, the resource-based policy that controls who can invoke it, VPC placement for private network access, and secrets management for sensitive configuration. Each layer is independent, and a mistake in any one of them creates a real security gap.
In production, Lambda security failures almost always come from the same small set of mistakes: overly broad execution roles, secrets stored in environment variables, VPC-connected functions that silently timeout because they cannot reach AWS services, or missing source-arn conditions on resource policies. This page walks through each layer in plain English so you can set them up correctly from the start.
Simple explanation
When a Lambda function runs, two separate questions need answers. First: who is allowed to start this function? Second: what is this function allowed to do once it is running?
These questions are controlled by different mechanisms. The resource-based policy answers the first question. The execution role answers the second. On top of those, VPC placement controls which private networks the function can reach, and secrets management controls how sensitive values like passwords and API keys get into the function safely.
All four layers need to be correct. A function with a perfectly scoped execution role but a misconfigured resource policy will not run. A function with the right invocation permission but an overly broad execution role is a security risk. They do not compensate for each other.
A Lambda function is like an employee at a secure facility. The resource-based policy is the building access system: it decides who is allowed through the door to trigger the function. The execution role is the job description: it lists exactly which systems that employee can touch once inside. The VPC is the floor plan, controlling which rooms they can physically reach. Secrets Manager is the key cabinet: it holds credentials to sensitive systems, and only the right employees can check them out.
How Lambda security works
Every Lambda invocation follows this sequence:
- Invocation request arrives from API Gateway, S3, an SDK call, an EventBridge schedule, or another AWS service.
- Resource-based policy check. Lambda checks whether the caller has permission to invoke this function. If not, the request is rejected before any code runs.
- Lambda assumes the execution role. Lambda uses AWS STS to get temporary credentials for the IAM role attached to the function.
- Function code runs. Your handler executes. Every AWS API call your code makes is authorized or denied based on the execution role’s permissions.
- Network path. If the function is in a VPC, outbound traffic flows through your private subnets and security groups. If not, it goes through AWS’s managed network with full internet access.
- Secrets access. If the function fetches secrets from Secrets Manager or SSM Parameter Store, those calls are authorized by the execution role and routed through the network path described above.
- Logging. Lambda writes logs to CloudWatch Logs, which also requires execution role permissions. Missing CloudWatch permissions means logs are silently dropped.
The 4 layers of Lambda security
| Layer | What it controls | Common mistake | Best practice |
|---|---|---|---|
| Execution role | What AWS services the function can call | Attaching broad managed policies like AmazonS3FullAccess | Scope every permission to the exact resource ARN |
| Resource-based policy | Who or what can invoke the function | Omitting source-arn / source-account on service principals | Always scope invocation grants to a specific source ARN |
| VPC / network | Which private networks the function can reach | Adding a VPC without VPC endpoints for AWS services | Only attach a VPC when needed; add endpoints for every service the function calls |
| Secrets / config | How sensitive values reach the function safely | Storing passwords in environment variables | Use Secrets Manager; retrieve and cache outside the handler |
Execution role: what your function can do
Every Lambda function has an execution role: an IAM role that Lambda assumes when the function runs. This role determines what AWS services and resources the function can call.
An IAM role has two parts that are easy to confuse:
- Trust policy. Defines who is allowed to assume this role. For a Lambda execution role, the trusted principal must be
lambda.amazonaws.com. Without this, Lambda cannot assume the role and the function will not start. - Permissions policy. Defines what this role can do once assumed. This is where you grant access to DynamoDB, S3, Secrets Manager, or whatever the function actually needs. See the IAM policy structure guide for how these policies are written.
# Create a Lambda execution role with the correct trust policy
aws iam create-role \
--role-name my-lambda-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach minimum CloudWatch Logs permissions
aws iam attach-role-policy \
--role-name my-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRoleAWSLambdaBasicExecutionRole is the minimum every function needs. It grants permission to write logs to CloudWatch Logs. Without it, your function runs but produces no output you can inspect. See monitoring Lambda for what else you should be capturing.
Beyond CloudWatch Logs, add only the permissions the function actually needs. Follow the principle of least privilege and scope every permission to the minimum resource ARN, not a wildcard:
# Least-privilege inline policy for a specific function
aws iam put-role-policy \
--role-name my-lambda-role \
--policy-name FunctionSpecificPermissions \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/my-table"
},
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::my-output-bucket/*"
}
]
}'For Lambda functions attached to a VPC, also attach AWSLambdaVPCAccessExecutionRole. This grants the ENI-related permissions Lambda needs to connect to your private subnets. Without it, the function cannot start in a VPC. Once attached, add your own least-privilege permissions on top for the actual work the function does.
Never attach AdministratorAccess or broad wildcard policies to a Lambda execution role. A compromised function with admin permissions can create IAM users, exfiltrate data from any S3 bucket, or launch EC2 instances. Scope every permission to the exact resources the function needs. The least privilege guide has practical patterns for writing tight Lambda policies.
Resource-based policy: who can invoke your function
The execution role controls what the function can do outbound. The resource-based policy controls who can invoke the function inbound. These are entirely separate mechanisms.
When you add a trigger via the console (API Gateway, S3, SNS, EventBridge), AWS adds the resource-based policy permission automatically. When configuring via the CLI or infrastructure as code, you add it manually with aws lambda add-permission.
# Allow API Gateway to invoke the function
aws lambda add-permission \
--function-name my-api-function \
--statement-id allow-api-gateway \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123/*/*"
# Allow S3 to invoke the function on object uploads
aws lambda add-permission \
--function-name my-s3-processor \
--statement-id allow-s3 \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn "arn:aws:s3:::my-input-bucket" \
--source-account "123456789012"
# Allow SNS to invoke the function from a specific topic
aws lambda add-permission \
--function-name my-sns-processor \
--statement-id allow-sns \
--action lambda:InvokeFunction \
--principal sns.amazonaws.com \
--source-arn "arn:aws:sns:us-east-1:123456789012:my-topic"
# Allow a specific IAM role in another account to invoke
aws lambda add-permission \
--function-name my-shared-function \
--statement-id allow-partner-account \
--action lambda:InvokeFunction \
--principal "arn:aws:iam::987654321098:role/PartnerServiceRole"View the current resource policy on any function:
aws lambda get-policy --function-name my-api-functionIf you allow s3.amazonaws.com to invoke your function without scoping to a specific bucket ARN, any S3 bucket in any AWS account that knows your function ARN could invoke it. Always include —source-arn when granting invocation rights to a service principal. For cross-account access, name the specific role rather than the account root. See IAM policy conditions for how to structure these safely.
VPC and network security
By default, Lambda functions do not run inside your VPC. They run in an AWS-managed network environment with outbound internet access and access to all public AWS API endpoints. For most functions, this is the right setup.
Does your function need to reach a private RDS database, an ElastiCache cluster, or an internal service that has no public endpoint? Add it to a VPC. If it only calls public AWS APIs like S3, DynamoDB, or SQS, keep it outside a VPC. You will get simpler networking, no extra cold start overhead, and nothing extra to manage.
When to attach Lambda to a VPC
Attach Lambda to your VPC when the function needs to reach private VPC resources. The serverless VPC access guide covers full setup details. The short version:
aws lambda update-function-configuration \
--function-name my-db-function \
--vpc-config '{
"SubnetIds": ["subnet-private-1a", "subnet-private-1b"],
"SecurityGroupIds": ["sg-lambda-outbound"]
}'Use private subnets (not public) for Lambda VPC placement. The security group attached to Lambda controls its outbound traffic. The security group on your RDS instance must allow inbound from the Lambda security group on the database port.
When a Lambda function is in a VPC, it loses default internet access and cannot reach public AWS service endpoints. A VPC-placed function that silently times out is almost always failing because it cannot reach Secrets Manager, SQS, SSM, or another AWS service. Fix this with VPC endpoints for each required service, or route internet-bound traffic through a NAT Gateway.
VPC endpoints for Lambda in private subnets
| Service your Lambda needs | VPC endpoint type | Endpoint name |
|---|---|---|
| S3 | Gateway (free) | com.amazonaws.region.s3 |
| DynamoDB | Gateway (free) | com.amazonaws.region.dynamodb |
| Secrets Manager | Interface | com.amazonaws.region.secretsmanager |
| SSM Parameter Store | Interface | com.amazonaws.region.ssm |
| SQS | Interface | com.amazonaws.region.sqs |
| SNS | Interface | com.amazonaws.region.sns |
| Lambda (self-invocation) | Interface | com.amazonaws.region.lambda |
Gateway endpoints (S3, DynamoDB) are free with no per-hour charge. Interface endpoints cost around $0.01/hour per Availability Zone. If the function needs broad internet access rather than specific AWS services, a NAT Gateway in a public subnet is often simpler than creating individual endpoints for every service.
Secrets and configuration
Environment variables vs Secrets Manager
Environment variables are fine for non-sensitive configuration: feature flags, base URLs, log levels, environment names. They are encrypted at rest using AWS KMS. The problem is not encryption at rest; it is visibility.
Encrypted at rest does not mean private. Anyone with IAM permissions to call aws lambda get-function-configuration sees environment variable values in plain text in the response. Do not put passwords, API keys, database credentials, or tokens in environment variables, even if you have added a custom KMS key.
Store actual secrets in AWS Secrets Manager and retrieve them at runtime. This keeps secrets out of function configuration, allows rotation without redeployment, and provides an audit trail of every access.
import boto3
import json
# Initialise the client once, outside the handler (reused on warm starts)
secrets_client = boto3.client('secretsmanager')
_db_credentials = None
def get_db_credentials():
global _db_credentials
if _db_credentials is None:
response = secrets_client.get_secret_value(SecretId='prod/myapp/database')
_db_credentials = json.loads(response['SecretString'])
return _db_credentials
def handler(event, context):
creds = get_db_credentials()
# Use creds['username'] and creds['password']Caching the secret outside the handler is important. Calling Secrets Manager on every invocation adds latency and cost. The pattern above calls Secrets Manager once per cold start, then reuses the cached value for all subsequent warm invocations.
The execution role needs permission to access the specific secret:
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/database-*"
}The trailing -* in the resource ARN matches the random suffix that Secrets Manager appends to secret names. Omitting it causes permission denied errors even when the secret name looks correct.
Execution role vs resource-based policy
This is the most common source of confusion when people start working with Lambda. Here is a direct comparison:
| Execution role | Resource-based policy | |
|---|---|---|
| What it controls | What the function can do | Who can invoke the function |
| Direction | Outbound (function to AWS services) | Inbound (caller to function) |
| Attached to | The IAM role | The Lambda function itself |
| Required? | Yes, Lambda cannot run without one | No, only needed for non-account callers |
| Grants access to | S3, DynamoDB, Secrets Manager, etc. | API Gateway, S3, SNS, other AWS accounts |
| Modified with | aws iam commands | aws lambda add-permission |
Granting the execution role lambda:InvokeFunction on itself does not let other services invoke the function. The resource-based policy on the function controls inbound invocation. These two systems do not interact with each other.
When to use this security model
Different Lambda use cases require different layers. Here are common scenarios and which parts of the security model matter most:
Internal API (called by other Lambda functions in the same account)
The calling function’s execution role needs lambda:InvokeFunction on the target function ARN. No resource-based policy is required for same-account calls when the execution role already grants invoke. Keep both functions outside a VPC unless they need private resource access. The shared responsibility model applies here: AWS secures the infrastructure, you secure the permissions.
S3 file processor (triggered by bucket uploads)
Add a resource-based policy allowing s3.amazonaws.com to invoke the function, scoped to the specific bucket ARN and your account ID. The execution role needs s3:GetObject on the source bucket and write permissions on the destination. The function typically stays outside a VPC.
Lambda calling RDS in a private subnet
Place the function in the same VPC as the database, in private subnets. Attach AWSLambdaVPCAccessExecutionRole plus least-privilege permissions for the function’s actual work. Add VPC endpoints for Secrets Manager (to retrieve the database password) and CloudWatch Logs. Use security groups to allow inbound to RDS only from the Lambda security group on the database port.
Public Lambda Function URL
If using NONE auth, your function code must implement authentication. Validate JWT tokens, API keys, or request signatures before doing any real work. Consider API Gateway with a Cognito or Lambda authorizer instead. Never expose business logic through an unauthenticated Function URL without application-level auth in the code.
Lambda Function URL vs API Gateway
| Lambda Function URL | API Gateway | |
|---|---|---|
| Auth options | AWS_IAM or NONE | IAM, Cognito, Lambda authorizer, API keys |
| Rate limiting | None built in | Throttling and usage plans |
| Request validation | None | Built-in JSON schema validation |
| Custom domain | Not supported | Supported |
| CORS | Configurable | Configurable per route |
| Best for | Internal calls, simple webhooks with IAM auth | Public APIs needing auth, throttling, or validation |
Function URLs are the right choice for internal service-to-service calls where the caller can sign requests with AWS credentials (AWS_IAM auth), or for simple webhooks where you own all callers. For anything public-facing or where you need more control, use API Gateway. See choosing between EC2, Lambda, and containers for broader guidance on when Lambda fits a given workload.
Common mistakes
- Broad execution roles. Attaching
AmazonS3FullAccesswhen the function only reads from one bucket, or usingResource: ”*”on DynamoDB. Every policy statement should name the exact resource ARN. The least privilege guide has patterns for writing tight permissions. - Secrets in environment variables. Even with KMS encryption, environment variable values appear in plain text to anyone who can describe the Lambda function configuration. Passwords, tokens, and API keys belong in Secrets Manager.
- VPC without VPC endpoints. Placing a function in a private subnet and expecting it to reach Secrets Manager, SQS, or other AWS services. Those calls fail silently with a timeout. Add a VPC endpoint for every AWS service the function needs to call, or route traffic through a NAT Gateway.
- Missing source-arn. Granting
s3.amazonaws.comorsns.amazonaws.cominvoke permission without asource-arncondition. Without it, any bucket or topic in any account that knows your function ARN can invoke it. - Confusing execution role with invocation permissions. Giving the execution role
lambda:InvokeFunctionon itself does not control who can trigger the function. The resource-based policy on the function controls inbound invocation. These are separate systems that do not interact. - Public Function URLs with no application auth. Setting
auth-type NONEand assuming the URL is obscure enough to be safe. URLs leak. Implement authentication in the function code, useAWS_IAMauth, or use API Gateway with a proper authorizer.
Summary
- Lambda security has four layers: execution role, resource-based policy, VPC/network, and secrets management. All four need to be correct; they do not compensate for each other.
- The execution role controls what the function can do (outbound). The resource-based policy controls who can invoke the function (inbound). They are separate mechanisms that work independently.
- Apply least privilege to execution roles and scope every permission to the exact resource ARN. For VPC-connected Lambda, start with
AWSLambdaVPCAccessExecutionRole, then add your own permissions. - Only place Lambda in a VPC when the function needs private VPC resources. Always add VPC endpoints for every AWS service the function calls from inside the VPC.
- Do not store secrets in environment variables. Use Secrets Manager, retrieve at runtime, and cache the result outside the handler to avoid per-invocation API calls.
- Always include
source-arnandsource-accounton resource-based policies that grant AWS service principals invocation rights. - For public Function URLs, implement application-level authentication in your function code, or use API Gateway with a proper authorizer instead.
Frequently asked questions
What is the difference between an execution role and a resource-based policy?
The execution role controls what your Lambda function can do: which AWS services it can call while running. The resource-based policy controls who can invoke your function in the first place. The execution role is about outbound access (function to AWS services). The resource-based policy is about inbound access (caller to function). A mistake in either one breaks things in a completely different way.
Should I put my Lambda function in a VPC?
Only if the function needs to access private VPC resources such as an RDS database, an ElastiCache cluster, or an internal service. Functions outside a VPC have full internet and AWS API access by default. Adding a VPC increases cold start latency slightly and requires VPC endpoints for each AWS service the function needs to call. Missing these causes silent timeouts that are hard to diagnose.
Are environment variables safe for storing secrets?
Not for sensitive values. Environment variables are encrypted at rest, but they appear in plain text in the Lambda console and via the AWS CLI to anyone with IAM permissions to describe the function configuration. Store passwords, API keys, and tokens in AWS Secrets Manager instead. Retrieve them at runtime and cache the result outside the handler so you are not calling Secrets Manager on every invocation.
What does AWSLambdaVPCAccessExecutionRole do?
It is an AWS managed policy that grants Lambda the permissions it needs to create, describe, and delete Elastic Network Interfaces (ENIs) in your VPC. Without it, Lambda cannot attach to your private subnets and the function will fail to start. Attach this policy when placing a function in a VPC, then add your own least-privilege permissions on top for the actual work the function does.
Should I use a Lambda Function URL or API Gateway?
For internal service-to-service calls or simple webhooks where you control all callers and can use AWS_IAM auth, a Function URL is simpler and sufficient. For public APIs that need rate limiting, request validation, auth integrations (Cognito, Lambda authorizers), custom domain names, or caching, API Gateway gives you more control. Never use a public Function URL with NONE auth unless your function implements its own authentication in code.