15 KiB
Serverless Security Best Practices
Security best practices for serverless applications based on AWS Well-Architected Framework.
Table of Contents
- Shared Responsibility Model
- Identity and Access Management
- Function Security
- API Security
- Data Protection
- Network Security
Shared Responsibility Model
Serverless Shifts Responsibility to AWS
With serverless, AWS takes on more security responsibilities:
AWS Responsibilities:
- Compute infrastructure
- Execution environment
- Runtime language and patches
- Networking infrastructure
- Server software and OS
- Physical hardware and facilities
- Automatic security patches (like Log4Shell mitigation)
Customer Responsibilities:
- Function code and dependencies
- Resource configuration
- Identity and Access Management (IAM)
- Data encryption (at rest and in transit)
- Application-level security
- Secure coding practices
Benefits of Shifted Responsibility
- Automatic Patching: AWS applies security patches automatically (e.g., Log4Shell fixed within 3 days)
- Infrastructure Security: No OS patching, server hardening, or vulnerability scanning
- Operational Agility: Quick security response at scale
- Focus on Code: Spend time on business logic, not infrastructure security
Identity and Access Management
Least Privilege Principle
Always use least privilege IAM policies:
// ✅ GOOD - Specific grant
const table = new dynamodb.Table(this, 'Table', {});
const function = new lambda.Function(this, 'Function', {});
table.grantReadData(function); // Only read access
// ❌ BAD - Overly broad
function.addToRolePolicy(new iam.PolicyStatement({
actions: ['dynamodb:*'],
resources: ['*'],
}));
Function Execution Role
Separate roles per function:
// ✅ GOOD - Each function has its own role
const readFunction = new NodejsFunction(this, 'ReadFunction', {
entry: 'src/read.ts',
// Gets its own execution role
});
const writeFunction = new NodejsFunction(this, 'WriteFunction', {
entry: 'src/write.ts',
// Gets its own execution role
});
table.grantReadData(readFunction);
table.grantReadWriteData(writeFunction);
// ❌ BAD - Shared role with excessive permissions
const sharedRole = new iam.Role(this, 'SharedRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'), // Too broad!
],
});
Resource-Based Policies
Control who can invoke functions:
// Allow API Gateway to invoke function
myFunction.grantInvoke(new iam.ServicePrincipal('apigateway.amazonaws.com'));
// Allow specific account
myFunction.addPermission('AllowAccountInvoke', {
principal: new iam.AccountPrincipal('123456789012'),
action: 'lambda:InvokeFunction',
});
// Conditional invoke (only from specific VPC endpoint)
myFunction.addPermission('AllowVPCInvoke', {
principal: new iam.ServicePrincipal('lambda.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceArn: vpcEndpoint.vpcEndpointId,
});
IAM Policies Best Practices
- Use grant methods: Prefer
.grantXxx()over manual policies - Condition keys: Use IAM conditions for fine-grained control
- Resource ARNs: Always specify resource ARNs, avoid wildcards
- Session policies: Use for temporary elevated permissions
- Service Control Policies (SCPs): Enforce organization-wide guardrails
Function Security
Lambda Isolation Model
Each function runs in isolated sandbox:
- Built on Firecracker microVMs
- Dedicated execution environment per function
- No shared memory between functions
- Isolated file system and network namespace
- Strong workload isolation
Execution Environment Security:
- One concurrent invocation per environment
- Environment may be reused (warm starts)
/tmpstorage persists between invocations- Sensitive data in memory may persist
Secure Coding Practices
Handle sensitive data securely:
// ✅ GOOD - Clean up sensitive data
export const handler = async (event: any) => {
const apiKey = process.env.API_KEY;
try {
const result = await callApi(apiKey);
return result;
} finally {
// Clear sensitive data from memory
delete process.env.API_KEY;
}
};
// ✅ GOOD - Use Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsClient = new SecretsManagerClient({});
export const handler = async (event: any) => {
const secret = await secretsClient.send(
new GetSecretValueCommand({ SecretId: process.env.SECRET_ARN })
);
const apiKey = secret.SecretString;
// Use apiKey
};
Dependency Management
Scan dependencies for vulnerabilities:
// package.json
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix"
},
"devDependencies": {
"snyk": "^1.0.0"
}
}
Keep dependencies updated:
- Run
npm auditorpip-auditregularly - Use Dependabot or Snyk for automated scanning
- Update dependencies promptly when vulnerabilities found
- Use minimal dependency sets
Environment Variable Security
Never store secrets in environment variables:
// ❌ BAD - Secret in environment variable
new NodejsFunction(this, 'Function', {
environment: {
API_KEY: 'sk-1234567890abcdef', // Never do this!
},
});
// ✅ GOOD - Reference to secret
new NodejsFunction(this, 'Function', {
environment: {
SECRET_ARN: secret.secretArn,
},
});
secret.grantRead(myFunction);
API Security
API Gateway Security
Authentication and Authorization:
// Cognito User Pool authorizer
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'Authorizer', {
cognitoUserPools: [userPool],
});
api.root.addMethod('GET', integration, {
authorizer,
authorizationType: apigateway.AuthorizationType.COGNITO,
});
// Lambda authorizer for custom auth
const customAuthorizer = new apigateway.TokenAuthorizer(this, 'CustomAuth', {
handler: authorizerFunction,
resultsCacheTtl: Duration.minutes(5),
});
// IAM authorization for service-to-service
api.root.addMethod('POST', integration, {
authorizationType: apigateway.AuthorizationType.IAM,
});
Request Validation
Validate requests at API Gateway:
const validator = new apigateway.RequestValidator(this, 'Validator', {
api,
validateRequestBody: true,
validateRequestParameters: true,
});
const model = api.addModel('Model', {
schema: {
type: apigateway.JsonSchemaType.OBJECT,
required: ['email', 'name'],
properties: {
email: {
type: apigateway.JsonSchemaType.STRING,
format: 'email',
},
name: {
type: apigateway.JsonSchemaType.STRING,
minLength: 1,
maxLength: 100,
},
},
},
});
resource.addMethod('POST', integration, {
requestValidator: validator,
requestModels: {
'application/json': model,
},
});
Rate Limiting and Throttling
const api = new apigateway.RestApi(this, 'Api', {
deployOptions: {
throttlingRateLimit: 1000, // requests per second
throttlingBurstLimit: 2000, // burst capacity
},
});
// Per-method throttling
resource.addMethod('POST', integration, {
methodResponses: [{ statusCode: '200' }],
requestParameters: {
'method.request.header.Authorization': true,
},
throttling: {
rateLimit: 100,
burstLimit: 200,
},
});
API Keys and Usage Plans
const apiKey = api.addApiKey('ApiKey', {
apiKeyName: 'customer-key',
});
const plan = api.addUsagePlan('UsagePlan', {
name: 'Standard',
throttle: {
rateLimit: 100,
burstLimit: 200,
},
quota: {
limit: 10000,
period: apigateway.Period.MONTH,
},
});
plan.addApiKey(apiKey);
plan.addApiStage({
stage: api.deploymentStage,
});
Data Protection
Encryption at Rest
DynamoDB encryption:
// Default: AWS-owned CMK (no additional cost)
const table = new dynamodb.Table(this, 'Table', {
encryption: dynamodb.TableEncryption.AWS_MANAGED, // AWS managed CMK
});
// Customer-managed CMK (for compliance)
const kmsKey = new kms.Key(this, 'Key', {
enableKeyRotation: true,
});
const table = new dynamodb.Table(this, 'Table', {
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
encryptionKey: kmsKey,
});
S3 encryption:
// SSE-S3 (default, no additional cost)
const bucket = new s3.Bucket(this, 'Bucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
});
// SSE-KMS (for fine-grained access control)
const bucket = new s3.Bucket(this, 'Bucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: kmsKey,
});
SQS/SNS encryption:
const queue = new sqs.Queue(this, 'Queue', {
encryption: sqs.QueueEncryption.KMS,
encryptionMasterKey: kmsKey,
});
const topic = new sns.Topic(this, 'Topic', {
masterKey: kmsKey,
});
Encryption in Transit
All AWS service APIs use TLS:
- API Gateway endpoints use HTTPS by default
- Lambda to AWS service communication encrypted
- EventBridge, SQS, SNS use TLS
- Custom domains can use ACM certificates
// API Gateway with custom domain
const certificate = new acm.Certificate(this, 'Certificate', {
domainName: 'api.example.com',
validation: acm.CertificateValidation.fromDns(hostedZone),
});
const api = new apigateway.RestApi(this, 'Api', {
domainName: {
domainName: 'api.example.com',
certificate,
},
});
Data Sanitization
Validate and sanitize inputs:
import DOMPurify from 'isomorphic-dompurify';
import { z } from 'zod';
// Schema validation
const OrderSchema = z.object({
orderId: z.string().uuid(),
amount: z.number().positive(),
email: z.string().email(),
});
export const handler = async (event: any) => {
const body = JSON.parse(event.body);
// Validate schema
const result = OrderSchema.safeParse(body);
if (!result.success) {
return {
statusCode: 400,
body: JSON.stringify({ error: result.error }),
};
}
// Sanitize HTML inputs
const sanitized = {
...result.data,
description: DOMPurify.sanitize(result.data.description),
};
await processOrder(sanitized);
};
Network Security
VPC Configuration
Lambda in VPC for private resources:
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
natGateways: 1,
});
// Lambda in private subnet
const vpcFunction = new NodejsFunction(this, 'VpcFunction', {
entry: 'src/handler.ts',
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [securityGroup],
});
// Security group for Lambda
const securityGroup = new ec2.SecurityGroup(this, 'LambdaSG', {
vpc,
description: 'Security group for Lambda function',
allowAllOutbound: false, // Restrict outbound
});
// Only allow access to RDS
securityGroup.addEgressRule(
ec2.Peer.securityGroupId(rdsSecurityGroup.securityGroupId),
ec2.Port.tcp(3306),
'Allow MySQL access'
);
VPC Endpoints
Use VPC endpoints for AWS services:
// S3 VPC endpoint (gateway endpoint, no cost)
vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
});
// DynamoDB VPC endpoint (gateway endpoint, no cost)
vpc.addGatewayEndpoint('DynamoDBEndpoint', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
});
// Secrets Manager VPC endpoint (interface endpoint, cost applies)
vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
privateDnsEnabled: true,
});
Security Groups
Principle of least privilege for network access:
// Lambda security group
const lambdaSG = new ec2.SecurityGroup(this, 'LambdaSG', {
vpc,
allowAllOutbound: false,
});
// RDS security group
const rdsSG = new ec2.SecurityGroup(this, 'RDSSG', {
vpc,
allowAllOutbound: false,
});
// Allow Lambda to access RDS only
rdsSG.addIngressRule(
ec2.Peer.securityGroupId(lambdaSG.securityGroupId),
ec2.Port.tcp(3306),
'Allow Lambda access'
);
lambdaSG.addEgressRule(
ec2.Peer.securityGroupId(rdsSG.securityGroupId),
ec2.Port.tcp(3306),
'Allow RDS access'
);
Security Monitoring
CloudWatch Logs
Enable and encrypt logs:
new NodejsFunction(this, 'Function', {
entry: 'src/handler.ts',
logRetention: logs.RetentionDays.ONE_WEEK,
logGroup: new logs.LogGroup(this, 'LogGroup', {
encryptionKey: kmsKey, // Encrypt logs
retention: logs.RetentionDays.ONE_WEEK,
}),
});
CloudTrail
Enable CloudTrail for audit:
const trail = new cloudtrail.Trail(this, 'Trail', {
isMultiRegionTrail: true,
includeGlobalServiceEvents: true,
managementEvents: cloudtrail.ReadWriteType.ALL,
});
// Log Lambda invocations
trail.addLambdaEventSelector([{
includeManagementEvents: true,
readWriteType: cloudtrail.ReadWriteType.ALL,
}]);
GuardDuty
Enable GuardDuty for threat detection:
- Analyzes VPC Flow Logs, DNS logs, CloudTrail events
- Detects unusual API activity
- Identifies compromised credentials
- Monitors for cryptocurrency mining
Security Best Practices Checklist
Development
- Validate and sanitize all inputs
- Scan dependencies for vulnerabilities
- Use least privilege IAM permissions
- Store secrets in Secrets Manager or Parameter Store
- Never log sensitive data
- Enable encryption for all data stores
- Use environment variables for configuration, not secrets
Deployment
- Enable CloudTrail in all regions
- Configure VPC for sensitive workloads
- Use VPC endpoints for AWS service access
- Enable GuardDuty for threat detection
- Implement resource-based policies
- Use AWS WAF for API protection
- Enable access logging for API Gateway
Operations
- Monitor CloudTrail for unusual activity
- Set up alarms for security events
- Rotate secrets regularly
- Review IAM policies periodically
- Audit function permissions
- Monitor GuardDuty findings
- Implement automated security responses
Testing
- Test with least privilege policies
- Validate error handling for security failures
- Test input validation and sanitization
- Verify encryption configurations
- Test with malicious payloads
- Audit logs for security events
Summary
- Shared Responsibility: AWS handles infrastructure, you handle application security
- Least Privilege: Use IAM grant methods, avoid wildcards
- Encryption: Enable encryption at rest and in transit
- Input Validation: Validate and sanitize all inputs
- Dependency Security: Scan and update dependencies regularly
- Monitoring: Enable CloudTrail, GuardDuty, and CloudWatch
- Secrets Management: Use Secrets Manager, never environment variables
- Network Security: Use VPC, security groups, and VPC endpoints appropriately