Files
gh-zxkane-aws-skills/skills/aws-serverless-eda/references/security-best-practices.md
2025-11-30 09:08:38 +08:00

15 KiB

Serverless Security Best Practices

Security best practices for serverless applications based on AWS Well-Architected Framework.

Table of Contents

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

  1. Use grant methods: Prefer .grantXxx() over manual policies
  2. Condition keys: Use IAM conditions for fine-grained control
  3. Resource ARNs: Always specify resource ARNs, avoid wildcards
  4. Session policies: Use for temporary elevated permissions
  5. 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)
  • /tmp storage 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 audit or pip-audit regularly
  • 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