Tarek Cheikh
Founder & AWS Security Expert
AWS Lambda is the backbone of modern serverless architectures, executing billions of invocations daily across AWS customers. Because Lambda abstracts away the underlying infrastructure, teams often assume security is "handled." In reality, Lambda shifts the security responsibility -- you no longer patch operating systems, but you are fully responsible for function code, execution roles, network configuration, secrets handling, and access policies.
In November 2025, researchers documented an AI-accelerated breach campaign where attackers used large language models to analyze stolen AWS credentials and automatically craft Lambda function code injection payloads, deploying crypto miners across multiple accounts in under 10 minutes. Separately, Wiz Research identified thousands of Lambda Function URLs exposed to the internet with AuthType: NONE, creating unauthenticated entry points into private cloud environments. These incidents underscore that Lambda security requires deliberate, layered controls.
This guide covers 12 battle-tested Lambda security best practices, each with real AWS CLI commands, audit procedures, and the latest 2025-2026 updates including re:Invent 2025 announcements like Durable Functions and Tenant Isolation improvements.
Every Lambda function assumes an IAM execution role that defines what AWS services and resources it can access. The most common mistake is sharing a single overly permissive role across multiple functions. If any one function is compromised, the attacker inherits all permissions granted to that shared role.
dynamodb:* on *.# Create a scoped execution role for a single function
aws iam create-role --role-name order-processor-lambda-role --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach only the permissions this specific function needs
aws iam put-role-policy --role-name order-processor-lambda-role --policy-name order-processor-policy --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Orders"
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/order-processor:*"
}
]
}'
# Generate a least-privilege policy from CloudTrail activity
aws accessanalyzer start-policy-generation --policy-generation-details '{"principalArn":"arn:aws:iam::123456789012:role/order-processor-lambda-role"}' --cloud-trail-details '{"trails":[{"cloudTrailArn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/my-trail","allRegions":true}],"accessRole":"arn:aws:iam::123456789012:role/AccessAnalyzerRole","startTime":"2025-09-01T00:00:00Z","endTime":"2026-03-01T00:00:00Z"}'
CIS Benchmark: Control 2.1.2 (Lambda functions should not have overly permissive execution roles). AWS Security Hub evaluates this automatically when the AWS Foundational Security Best Practices standard is enabled.
Lambda environment variables are encrypted at rest by default using an AWS-managed KMS key. However, anyone with lambda:GetFunction permission can retrieve the decrypted values. For sensitive configuration, this default encryption is insufficient.
kms:Decrypt call in your function code.# Create a customer-managed KMS key for Lambda
aws kms create-key --description "Lambda environment variable encryption" --key-usage ENCRYPT_DECRYPT
# Update function to use the CMK for at-rest encryption
aws lambda update-function-configuration --function-name order-processor --kms-key-arn arn:aws:kms:us-east-1:123456789012:key/abcd1234-ef56-gh78-ij90-klmnopqrstuv
# Audit: list functions and their KMS key configuration
aws lambda list-functions --query "Functions[?KMSKeyArn==null].[FunctionName]" --output table
Caveat: Environment variables are limited to 4 KB total. For larger configuration payloads, use AWS Systems Manager Parameter Store or Secrets Manager.
By default, Lambda functions run in an AWS-managed VPC with internet access but no access to your private VPC resources (RDS instances, ElastiCache clusters, internal APIs). If your function needs to reach private resources, you must configure VPC placement.
# Create a dedicated security group for the Lambda function
aws ec2 create-security-group --group-name lambda-order-processor-sg --description "Security group for order-processor Lambda" --vpc-id vpc-0abc123def456
# Allow outbound access only to the RDS instance on port 5432
aws ec2 authorize-security-group-egress --group-id sg-0abc123 --protocol tcp --port 5432 --source-group sg-0rds456
# Configure the Lambda function for VPC access
aws lambda update-function-configuration --function-name order-processor --vpc-config SubnetIds=subnet-0priv1,subnet-0priv2,SecurityGroupIds=sg-0abc123
# Create a VPC endpoint for Secrets Manager (avoid NAT for AWS services)
aws ec2 create-vpc-endpoint --vpc-id vpc-0abc123def456 --service-name com.amazonaws.us-east-1.secretsmanager --vpc-endpoint-type Interface --subnet-ids subnet-0priv1 subnet-0priv2 --security-group-ids sg-0vpce789
Performance note: Since the 2019 Hyperplane ENI improvements, VPC-configured Lambda functions no longer suffer cold start penalties for ENI attachment. ENIs are pre-created and shared across functions in the same security group and subnet combination.
Lambda Function URLs provide a dedicated HTTPS endpoint for invoking a function without API Gateway. They support two authentication modes: AWS_IAM and NONE. Wiz Research found thousands of Function URLs set to NONE, exposing internal functions to the public internet without any authentication.
AWS_IAM authentication. The NONE mode should be reserved for genuinely public endpoints (rare) and should be paired with application-level authentication.lambda:InvokeFunctionUrl and a resource-based policy allowing the caller. This dual-permission check prevents accidental exposure.# Create a Function URL with IAM authentication (correct)
aws lambda create-function-url-config --function-name order-api --auth-type AWS_IAM --cors '{"AllowOrigins":["https://app.example.com"],"AllowMethods":["GET","POST"]}'
# Audit: find functions with NONE authentication (security risk)
aws lambda list-functions --query "Functions[].FunctionName" --output text | tr ' ' '
' | while read fn; do
auth=$(aws lambda get-function-url-config --function-name "$fn" --query "AuthType" --output text 2>/dev/null)
if [ "$auth" = "NONE" ]; then
echo "WARNING: $fn has unauthenticated Function URL"
fi
done
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyUnauthenticatedFunctionURLs",
"Effect": "Deny",
"Action": "lambda:CreateFunctionUrlConfig",
"Resource": "*",
"Condition": {
"StringEquals": {
"lambda:FunctionUrlAuthType": "NONE"
}
}
}]
}
2025 Update: The October 2025 dual-permission model for Function URLs adds a second authorization check. New functions automatically require both IAM principal permissions and a resource-based policy grant. Existing functions created before October 2025 should be reviewed and migrated to the new model.
Lambda resource-based policies control which AWS principals and services can invoke a function. An overly permissive resource-based policy -- especially one with Principal: "*" -- can allow anyone on the internet (or any AWS account) to invoke your function.
Principal: "*" without a restrictive condition key such as aws:PrincipalOrgID or aws:SourceArn.# Review the resource-based policy on a function
aws lambda get-policy --function-name order-processor --query "Policy" --output text | python3 -m json.tool
# Audit all functions for overly permissive policies
aws lambda list-functions --query "Functions[].FunctionName" --output text | tr ' ' '
' | while read fn; do
policy=$(aws lambda get-policy --function-name "$fn" --query "Policy" --output text 2>/dev/null)
if echo "$policy" | grep -q '"Principal":"*"'; then
echo "CRITICAL: $fn has Principal: * in resource-based policy"
fi
done
# Remove an overly permissive statement
aws lambda remove-permission --function-name order-processor --statement-id overly-permissive-statement
CIS Benchmark: Ensure Lambda function policies do not grant public access. AWS Security Hub flags this under the AWS Foundational Security Best Practices standard (Lambda.1).
Lambda layers provide shared libraries, runtimes, and dependencies across functions. A malicious or compromised layer can inject code into every function that uses it. Supply chain attacks through layers are a growing concern.
lambda:AddLayerVersionPermission carefully, and audit who has access to publish new layer versions.# List layers and their permissions
aws lambda list-layers --query "Layers[].{Name:LayerName,ARN:LatestMatchingVersion.LayerVersionArn}"
# Check permissions on a specific layer version
aws lambda get-layer-version-policy --layer-name my-shared-lib --version-number 3
# Pin a specific layer version (correct)
aws lambda update-function-configuration --function-name order-processor --layers arn:aws:lambda:us-east-1:123456789012:layer:my-shared-lib:3
# Restrict layer sharing to your organization only
aws lambda add-layer-version-permission --layer-name my-shared-lib --version-number 3 --statement-id org-access --action lambda:GetLayerVersion --principal "*" --organization-id o-abc123def4
Code signing ensures that only trusted, unmodified code can be deployed to your Lambda functions. AWS Signer creates a cryptographic signature of your deployment package, and Lambda verifies this signature before allowing the code to run.
Enforce in production to block deployment of unsigned or tampered code.# Create a signing profile (valid for 3 years)
aws signer put-signing-profile --profile-name LambdaProductionSigning --platform-id AWSLambda-SHA384-ECDSA --signature-validity-period value=135,type=MONTHS
# Create a code signing configuration
aws lambda create-code-signing-config --allowed-publishers SigningProfileVersionArns=arn:aws:signer:us-east-1:123456789012:/signing-profiles/LambdaProductionSigning --code-signing-policies UntrustedArtifactOnDeployment=Enforce --description "Production Lambda code signing"
# Attach the code signing config to a function
aws lambda update-function-code-signing-config --function-name order-processor --code-signing-config-arn arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-abc123
# Sign a deployment package
aws signer start-signing-job --source 's3={bucketName=my-deployment-bucket,key=order-processor.zip,version=abc123}' --destination 's3={bucketName=my-signed-bucket,prefix=signed/}' --profile-name LambdaProductionSigning
Best practice: Integrate code signing into your CI/CD pipeline. The build step creates the deployment package, the signing step signs it with AWS Signer, and the deployment step uses the signed artifact. Any tampering between build and deployment is caught.
Without concurrency limits, a Lambda function can scale to consume your entire account's concurrency pool (default 1,000 concurrent executions per region). An attacker flooding one function with requests can starve all other functions in the account -- a form of denial-of-service.
# Set reserved concurrency to limit blast radius
aws lambda put-function-concurrency --function-name order-processor --reserved-concurrent-executions 100
# Emergency: disable a compromised function immediately
aws lambda put-function-concurrency --function-name compromised-function --reserved-concurrent-executions 0
# Audit: list functions without reserved concurrency
aws lambda list-functions --query "Functions[].FunctionName" --output text | tr ' ' '
' | while read fn; do
conc=$(aws lambda get-function-concurrency --function-name "$fn" --query "ReservedConcurrentExecutions" --output text 2>/dev/null)
if [ "$conc" = "None" ]; then
echo "WARNING: $fn has no reserved concurrency"
fi
done
Best practice: Leave at least 10% of your account concurrency unreserved so new or unscoped functions can still execute. Monitor the ConcurrentExecutions and Throttles CloudWatch metrics to right-size your concurrency limits.
When an asynchronous Lambda invocation fails after all retries, the event is silently dropped unless you configure a Dead Letter Queue (DLQ). Lost events mean lost visibility into failures, which can mask security-relevant errors such as permission denials, injection attempts, or data exfiltration failures.
# Create an SQS dead letter queue
aws sqs create-queue --queue-name order-processor-dlq
# Configure the DLQ on the Lambda function
aws lambda update-function-configuration --function-name order-processor --dead-letter-config TargetArn=arn:aws:sqs:us-east-1:123456789012:order-processor-dlq
# Better: use Lambda Destinations for richer failure context
aws lambda put-function-event-invoke-config --function-name order-processor --destination-config '{
"OnFailure": {
"Destination": "arn:aws:sqs:us-east-1:123456789012:order-processor-failures"
}
}'
# Create a CloudWatch alarm on the DLQ
aws cloudwatch put-metric-alarm --alarm-name order-processor-dlq-alarm --namespace AWS/SQS --metric-name ApproximateNumberOfMessagesVisible --dimensions Name=QueueName,Value=order-processor-dlq --statistic Sum --period 300 --threshold 1 --comparison-operator GreaterThanOrEqualToThreshold --evaluation-periods 1 --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts
Best practice: The execution role must have sqs:SendMessage or sns:Publish permission on the DLQ resource. A common misconfiguration is setting up the DLQ but forgetting the IAM permission, causing silent failures.
Lambda managed runtimes receive automatic security patches from AWS. How and when those patches apply depends on your runtime update configuration. Using outdated or end-of-life runtimes means your functions run on unpatched environments with known vulnerabilities.
# Check the runtime management configuration
aws lambda get-runtime-management-config --function-name order-processor
# Set to Auto mode (recommended)
aws lambda put-runtime-management-config --function-name order-processor --update-runtime-on Auto
# Audit: find functions using deprecated runtimes
aws lambda list-functions --query "Functions[?Runtime=='python3.8' || Runtime=='nodejs16.x' || Runtime=='dotnet6'].[FunctionName,Runtime]" --output table
CIS Benchmark: Ensure Lambda functions use supported runtimes. AWS Security Hub flags functions using deprecated runtimes under the AWS Foundational Security Best Practices standard (Lambda.2).
Storing secrets in Lambda environment variables -- even encrypted with KMS -- exposes them to anyone with lambda:GetFunction or lambda:GetFunctionConfiguration permissions. Secrets Manager provides automatic rotation, fine-grained access control, and audit logging for every secret retrieval.
# Create a secret in Secrets Manager
aws secretsmanager create-secret --name prod/order-processor/db-credentials --secret-string '{"username":"app_user","password":"REPLACE_ME"}' --kms-key-id alias/lambda-secrets-key
# Add the Secrets Manager extension layer to your function
aws lambda update-function-configuration --function-name order-processor --layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:12
# Grant the execution role permission to read the secret
aws iam put-role-policy --role-name order-processor-lambda-role --policy-name secrets-access --policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/order-processor/db-credentials-*"
}]
}'
# Enable automatic rotation (every 30 days)
aws secretsmanager rotate-secret --secret-id prod/order-processor/db-credentials --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:secret-rotation --rotation-rules AutomaticallyAfterDays=30
Powertools for AWS Lambda: The Powertools library (available for Python, TypeScript, Java, and .NET) provides a parameters utility that integrates with Secrets Manager and Parameter Store. It handles caching, decryption, and error handling out of the box. Additionally, Powertools offers data masking utilities that automatically redact sensitive fields from logs, and idempotency utilities that prevent duplicate processing of the same event -- both critical for security.
Lambda SnapStart (available for Java and Python) dramatically reduces cold start latency by creating a snapshot of the initialized function and reusing it across invocations. However, this creates two security hazards that must be addressed.
Any random values, unique identifiers, or cryptographic seeds generated during initialization are captured in the snapshot and reused identically across all invocations. This can lead to predictable UUIDs, repeated encryption nonces, and duplicate session tokens.
Secrets or credentials retrieved during initialization are frozen in the snapshot. If a secret is rotated after the snapshot is taken, the function continues using the stale credential until the snapshot is refreshed.
CRaC (Coordinated Restore at Checkpoint) hooks for Java to re-initialize random number generators and refresh connections after restore.# Enable SnapStart on a Java function
aws lambda update-function-configuration --function-name order-processor --snap-start ApplyOn=PublishedVersions
# Publish a new version to create the snapshot
aws lambda publish-version --function-name order-processor
# Verify SnapStart optimization status
aws lambda get-function --function-name order-processor --query "Configuration.SnapStart"
Best practice: After enabling SnapStart, audit your initialization code for any calls to SecureRandom, UUID.randomUUID(), or secret retrieval. Use the afterRestore CRaC hook to re-initialize these after snapshot restoration. For Python SnapStart, move all secret fetches and random seed generation into the handler function.
AWS re:Invent 2025 introduced several Lambda features with security implications:
| Misconfiguration | Risk | Detection |
|---|---|---|
| Shared execution role across multiple functions | One compromised function exposes all permissions | Audit: multiple functions referencing the same Role ARN |
Function URL with AuthType: NONE |
Unauthenticated public access to function | AWS Config: lambda-function-url-auth-type-check |
Principal: "*" in resource-based policy |
Any AWS account can invoke the function | IAM Access Analyzer external access findings |
| Secrets stored in environment variables | Exposed via lambda:GetFunctionConfiguration |
Code review; scan for plaintext secrets in env vars |
| No reserved concurrency configured | DoS can exhaust account-wide concurrency | Audit: GetFunctionConcurrency returns null |
| Deprecated runtime (e.g., Python 3.8, Node.js 16) | Unpatched vulnerabilities in runtime | Security Hub: Lambda.2 finding |
| VPC function with overly permissive security group | Unrestricted network access from function | Security group audit: 0.0.0.0/0 egress rules |
| SnapStart with secrets fetched during init | Stale credentials after secret rotation | Code review for init-phase secret retrieval |
| # | Practice | Priority |
|---|---|---|
| 1 | Execution role least privilege (one role per function) | Critical |
| 2 | Encrypt environment variables with CMK | High |
| 3 | VPC placement for private resource access | High |
| 4 | Function URL authentication (always AWS_IAM) | Critical |
| 5 | Audit resource-based policies (no Principal: *) | Critical |
| 6 | Secure Lambda layers (trusted sources, pin versions) | High |
| 7 | Enable code signing with AWS Signer | High |
| 8 | Reserved concurrency for DoS protection | High |
| 9 | Configure dead letter queues for failure visibility | Medium |
| 10 | Manage runtime updates (Auto mode) | High |
| 11 | Use Secrets Manager instead of environment variables | Critical |
| 12 | SnapStart security (uniqueness and secrets freshness) | Medium |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing AWS Identity and Access Management. Covers MFA enforcement, least privilege, IAM Identity Center, SCPs, Access Analyzer, and credential management.
Comprehensive guide to securing AWS EC2 instances. Covers IMDSv2 enforcement, security groups, EBS encryption, SSM Session Manager, private subnets, VPC Flow Logs, Amazon Inspector, and AMI hardening.
Comprehensive guide to securing Amazon RDS databases. Covers encryption at rest and in transit, private subnet deployment, IAM database authentication, RDS Proxy, audit logging, Secrets Manager rotation, and snapshot security.