AWS Lambda Security Hardening: A Complete Guide to Securing Serverless Functions
AWS Lambda's managed execution model eliminates server patching, but it does not eliminate the need for security hardening. Misconfigured Lambda functions are a common source of privilege escalation, data leakage, and lateral movement in cloud environments. This guide covers the essential security controls for production Lambda deployments.
Least-Privilege Execution Roles
Every Lambda function assumes an IAM execution role at runtime. The most common mistake is sharing a single overpermissive role across multiple functions.
Dedicated Per-Function Roles
import boto3
import json
def create_lambda_execution_role(function_name, allowed_actions):
"""Create a dedicated least-privilege execution role for a Lambda function"""
iam = boto3.client('iam')
assume_role_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": boto3.client('sts').get_caller_identity()['Account']
}
}
}
]
}
role = iam.create_role(
RoleName=f"lambda-{function_name}-role",
AssumeRolePolicyDocument=json.dumps(assume_role_policy),
Description=f"Execution role for {function_name} Lambda function"
)
# Attach only the specific permissions needed
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": f"arn:aws:logs:*:*:log-group:/aws/lambda/{function_name}:*"
},
{
"Effect": "Allow",
"Action": allowed_actions,
"Resource": "*"
}
]
}
iam.put_role_policy(
RoleName=role['Role']['RoleName'],
PolicyName=f"{function_name}-permissions",
PolicyDocument=json.dumps(policy_document)
)
return role['Role']['Arn']
Key principles:
- One role per function — never share roles across functions with different responsibilities
- Scope log permissions — restrict
logs:*to the specific log group for the function - Use resource-level constraints — replace
"Resource": "*"with specific ARNs wherever possible - Add source account conditions — prevent confused deputy attacks on the trust policy
VPC Configuration for Private Resources
Lambda functions that access databases, caches, or internal APIs should run inside a VPC. Functions that only call public AWS APIs generally should not — VPC placement adds cold start latency and requires NAT Gateway for internet access.
Secure VPC Lambda Configuration
# serverless.yml or SAM template excerpt
Resources:
SecureFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: secure-data-processor
Runtime: python3.12
Handler: handler.main
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# Reserved concurrency prevents runaway invocations
ReservedConcurrentExecutions: 100
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Lambda function security group
VpcId: !Ref VPC
SecurityGroupEgress:
# Only allow outbound to the database
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
DestinationSecurityGroupId: !Ref DatabaseSecurityGroup
# Allow outbound to VPC endpoints (HTTPS)
- IpProtocol: tcp
FromPort: 443
ToPort: 443
DestinationPrefixListId: !Ref S3PrefixList
# VPC endpoint for DynamoDB access without NAT
DynamoDBEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
VpcId: !Ref VPC
RouteTableIds:
- !Ref PrivateRouteTable
Best practices for VPC Lambda:
- Place functions in private subnets only — never in public subnets
- Use VPC endpoints for DynamoDB, S3, SQS, and other AWS services to avoid NAT costs
- Restrict security group egress rules to specific destinations and ports
- Deploy across multiple AZs for high availability
Environment Variable Encryption
Lambda environment variables are encrypted at rest by default with an AWS-managed key. For sensitive values, use a customer-managed KMS key and decrypt at runtime.
Runtime Decryption Pattern
import boto3
import os
import base64
from functools import lru_cache
kms_client = boto3.client('kms')
@lru_cache(maxsize=16)
def decrypt_env_var(encrypted_value):
"""Decrypt a KMS-encrypted environment variable with caching"""
decrypted = kms_client.decrypt(
CiphertextBlob=base64.b64decode(encrypted_value),
EncryptionContext={
'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']
}
)
return decrypted['Plaintext'].decode('utf-8')
def handler(event, context):
# Decrypt sensitive values — cached across warm invocations
db_password = decrypt_env_var(os.environ['ENCRYPTED_DB_PASSWORD'])
api_key = decrypt_env_var(os.environ['ENCRYPTED_API_KEY'])
# Use decrypted values
return process_event(event, db_password, api_key)
The @lru_cache decorator ensures KMS is only called once per cold start, keeping costs low and latency minimal.
Resource-Based Policies
Lambda resource-based policies control who can invoke a function. Without explicit restrictions, any principal with lambda:InvokeFunction permission on the function ARN can call it.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAPIGatewayInvoke",
"Effect": "Allow",
"Principal": {"Service": "apigateway.amazonaws.com"},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-api-handler",
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:execute-api:us-east-1:123456789012:abc123def4/*"
}
}
}
]
}
Always include a Condition block that restricts invocation to the expected source. Without it, any API Gateway in the account — or any account, if the principal is a wildcard — can invoke the function.
Monitoring and Threat Detection
Enable GuardDuty Lambda Protection to detect:
- Functions communicating with known command-and-control servers
- Cryptocurrency mining activity
- DNS queries to malicious domains from VPC-attached functions
Use CloudWatch Logs Insights to detect anomalous patterns:
fields @timestamp, @message
| filter @message like /AccessDenied/
| stats count() as denied_count by bin(1h) as hour
| sort denied_count desc
Frequent AccessDenied errors may indicate a compromised function probing for permissions beyond its role.
Securing Lambda Functions with AccessLens
Lambda execution roles are one of the most common sources of IAM privilege escalation. An overpermissive Lambda role combined with code injection can give an attacker broad access to your AWS environment.
AccessLens helps secure your Lambda deployment by providing:
- Execution role analysis that identifies overpermissive Lambda roles and unused permissions
- Cross-service access mapping that shows which resources each Lambda function can reach
- Privilege escalation detection that finds Lambda roles capable of modifying IAM policies or assuming other roles
- Trust relationship visualization that reveals unexpected invocation paths between services and accounts
Your Lambda functions are only as secure as the IAM roles they assume. AccessLens gives you the visibility to enforce true least privilege across your serverless workloads.
Secure your Lambda functions with AccessLens and eliminate the IAM blind spots in your serverless architecture.