Serverless Security Hardening
Serverless removes server patching from your plate but introduces new attack surfaces: overly permissive IAM roles, injection through event sources, dependency vulnerabilities in Lambda packages, and exposed API endpoints. We harden your serverless application against the OWASP Serverless Top 10 with IAM least-privilege enforcement, input validation, dependency scanning in CI, WAF rules, and secrets rotation — so your application meets security audit requirements.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
IAM Least-Privilege Enforcement
The most critical serverless security control is ensuring every Lambda function has only the permissions it needs. We audit and restrict IAM policies to exact resource ARNs and actions.
# BAD — overly permissive (common in tutorials)
{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
}
# GOOD — scoped to exact table and actions
resource "aws_iam_role_policy" "order_handler" {
role = aws_iam_role.order_handler.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadWriteOrdersTable"
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
]
Resource = [
aws_dynamodb_table.orders.arn,
"${aws_dynamodb_table.orders.arn}/index/*",
]
},
{
Sid = "PublishToOrderEvents"
Effect = "Allow"
Action = ["events:PutEvents"]
Resource = aws_cloudwatch_event_bus.main.arn
Condition = {
StringEquals = {
"events:source" = "orders" # Can only publish as 'orders' source
}
}
}
]
})
}
# IAM Access Analyzer — detect unused permissions
resource "aws_accessanalyzer_analyzer" "main" {
analyzer_name = "${var.project}-analyzer"
type = "ACCOUNT"
}We enable IAM Access Analyzer to continuously monitor for unused permissions and overly broad policies. Every function gets its own IAM role — we never share roles across functions with different access patterns. Condition keys restrict actions to specific event sources, encryption keys, or VPC endpoints.
Input Validation & Injection Prevention
Every event source (API Gateway, SQS, S3, EventBridge) is an input vector. We validate and sanitize all inputs before processing.
import { z } from 'zod';
// Strict input schemas for every endpoint
const CreateOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
sku: z.string().regex(/^[A-Z0-9-]{3,20}$/),
quantity: z.number().int().min(1).max(100),
})).min(1).max(50),
shippingAddress: z.object({
line1: z.string().min(1).max(200),
city: z.string().min(1).max(100),
country: z.string().length(2),
postal: z.string().regex(/^[A-Z0-9 -]{3,10}$/i),
}),
// Reject unexpected fields
}).strict();
export function validateInput<T>(schema: z.ZodSchema<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
throw new ValidationError(
result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`)
);
}
return result.data;
}
// In handler
export const handler = async (event: APIGatewayProxyEventV2) => {
let body: unknown;
try {
body = JSON.parse(event.body || '{}');
} catch {
return { statusCode: 400, body: 'Invalid JSON' };
}
const input = validateInput(CreateOrderSchema, body);
// input is now typed and validated
};We use Zod for runtime schema validation with .strict() to reject unexpected fields (preventing mass assignment attacks). SQL injection is prevented by using parameterized queries exclusively — Prisma and DynamoDB SDKs handle this automatically. For S3 event triggers, we validate file names, sizes, and content types before processing.
Dependency Scanning & WAF
We integrate dependency vulnerability scanning into your CI pipeline and configure AWS WAF for API protection.
# GitHub Actions — dependency audit
- name: Audit dependencies
run: |
npm audit --audit-level=high
npx better-npm-audit audit --level high
# Snyk integration for deeper scanning
- uses: snyk/actions/node@master
with:
command: test
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# Dependabot configuration
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
groups:
aws-sdk:
patterns: ["@aws-sdk/*"]
# AWS WAF for API Gateway
resource "aws_wafv2_web_acl" "api" {
name = "api-protection-${var.env}"
scope = "REGIONAL"
default_action { allow {} }
rule {
name = "aws-managed-common"
priority = 1
override_action { none {} }
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "common-rules"
}
}
rule {
name = "aws-managed-sqli"
priority = 2
override_action { none {} }
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "sqli-rules"
}
}
}AWS WAF managed rule groups block common attacks (XSS, SQL injection, bad bots) at the edge before requests reach your Lambda functions. We configure WAF in count mode first to identify false positives, then switch to block mode after tuning. Dependabot groups related packages to reduce PR noise.
Secrets Management & Encryption
We implement secure secrets management with automatic rotation and encryption at rest using customer-managed KMS keys.
resource "aws_kms_key" "lambda" {
description = "Lambda environment variable encryption"
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowLambdaDecrypt"
Effect = "Allow"
Principal = { AWS = aws_iam_role.lambda_exec.arn }
Action = ["kms:Decrypt"]
Resource = "*"
}
]
})
}
# Secrets Manager with rotation
resource "aws_secretsmanager_secret" "db_password" {
name = "${var.project}/db-password/${var.env}"
kms_key_id = aws_kms_key.lambda.id
}
resource "aws_secretsmanager_secret_rotation" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
rotation_lambda_arn = aws_lambda_function.rotate_secret.arn
rotation_rules {
automatically_after_days = 30
}
}
# Lambda caches secrets to avoid per-invocation API calls
const secretCache = new Map<string, { value: string; expires: number }>();
async function getSecret(name: string): Promise<string> {
const cached = secretCache.get(name);
if (cached && cached.expires > Date.now()) return cached.value;
const result = await secretsManager.send(
new GetSecretValueCommand({ SecretId: name })
);
secretCache.set(name, {
value: result.SecretString!,
expires: Date.now() + 5 * 60 * 1000, // Cache 5 minutes
});
return result.SecretString!;
}Secrets are cached in Lambda memory for 5 minutes to avoid Secrets Manager API costs and latency on every invocation. KMS key rotation is enabled for automatic annual key rotation. Environment variables are encrypted with the customer-managed KMS key instead of the default Lambda key, giving you full control over the encryption lifecycle. We never store secrets in environment variables directly — they are always fetched from Secrets Manager at runtime.
Why Anubiz Engineering
Ready to get started?
Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.