Serverless computing has rapidly moved from an interesting experiment to a production-ready platform for building scalable applications. However, the security model for serverless applications differs significantly from traditional architectures. In this post, I’ll explore the unique security challenges of serverless environments and provide practical strategies for securing Function-as-a-Service (FaaS) deployments.
The Serverless Security Paradigm Shift
Traditional application security focuses on securing long-running processes, managing network perimeters, and patching operating systems. Serverless inverts many of these assumptions. Functions are ephemeral, the infrastructure is abstracted away, and the attack surface shifts from infrastructure to code and configuration.
This doesn’t mean serverless is inherently more or less secure—it’s differently secure. Understanding these differences is crucial for building robust serverless applications.
The Shared Responsibility Model
In serverless environments, the cloud provider manages significantly more of the security stack:
Provider Responsibilities:
- Physical security
- Hypervisor and container runtime security
- OS patching and updates
- Network infrastructure
- Function runtime environments
Your Responsibilities:
- Application code security
- Dependency management
- Identity and access management
- Data encryption
- Configuration security
- Third-party service integration
The shrinking of your responsibility doesn’t mean less work—it means different work, often requiring deeper expertise in application-level security.
Function-Level Security Considerations
Least Privilege Execution
Every function should have the minimum permissions required to perform its task. This is more critical in serverless than in traditional environments because the blast radius of a compromised function can be significant.
Here’s an example of overly permissive IAM configuration:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
Instead, scope permissions precisely:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::specific-bucket/specific-prefix/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:region:account:table/specific-table"
}
]
}
Input Validation and Injection Attacks
Functions often process data from untrusted sources: API requests, event streams, file uploads. Without proper validation, they’re vulnerable to injection attacks.
Consider a function that processes user input:
exports.handler = async (event) => {
const userId = event.queryStringParameters.userId;
// Vulnerable to SQL injection
const query = `SELECT * FROM users WHERE id = '${userId}'`;
const result = await db.execute(query);
return {
statusCode: 200,
body: JSON.stringify(result)
};
};
A secure implementation uses parameterized queries and validation:
const Joi = require('joi');
const userIdSchema = Joi.string().uuid().required();
exports.handler = async (event) => {
try {
// Validate input
const { error, value } = userIdSchema.validate(
event.queryStringParameters?.userId
);
if (error) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid user ID format' })
};
}
// Use parameterized queries
const result = await db.query(
'SELECT * FROM users WHERE id = ?',
[value]
);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (err) {
console.error('Error processing request:', err);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
Secrets Management
Hardcoding secrets in function code or environment variables is a critical vulnerability. Use dedicated secrets management services:
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
// Cache secrets outside handler for reuse across invocations
let cachedSecret = null;
async function getSecret(secretName) {
if (cachedSecret) {
return cachedSecret;
}
const data = await secretsManager.getSecretValue({
SecretId: secretName
}).promise();
cachedSecret = JSON.parse(data.SecretString);
return cachedSecret;
}
exports.handler = async (event) => {
const dbCredentials = await getSecret('database-credentials');
// Use credentials to connect to database
const connection = await createConnection({
host: dbCredentials.host,
user: dbCredentials.username,
password: dbCredentials.password
});
// Process request
};
Dependency Security
Serverless functions often include numerous npm packages or pip modules. Each dependency is a potential security risk.
Best Practices:
-
Minimize Dependencies: Only include what you actually need. Consider implementing simple functionality yourself rather than adding a library.
-
Lock Versions: Use package-lock.json or requirements.txt to ensure reproducible builds.
-
Automated Scanning: Integrate dependency scanning into your CI/CD pipeline:
# Example GitHub Actions workflow
name: Security Scan
on: [push]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run npm audit
run: npm audit --audit-level=moderate
- name: Run Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- Regular Updates: Keep dependencies current with security patches. Use tools like Dependabot or Renovate for automated updates.
Data Encryption
Encryption at Rest
Data stored in cloud services should be encrypted:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const params = {
Bucket: 'my-secure-bucket',
Key: 'sensitive-data.json',
Body: JSON.stringify(event.data),
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: 'arn:aws:kms:region:account:key/key-id'
};
await s3.putObject(params).promise();
};
Encryption in Transit
Always use TLS for external communications. For API endpoints, enforce HTTPS:
# API Gateway configuration
Resources:
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
EndpointConfiguration:
Types:
- REGIONAL
Policy:
Version: '2012-10-17'
Statement:
- Effect: Deny
Principal: '*'
Action: 'execute-api:Invoke'
Resource: '*'
Condition:
Bool:
'aws:SecureTransport': false
Timeout and Resource Limits
Unbounded execution can lead to denial-of-service scenarios or unexpected costs:
# Serverless Framework configuration
functions:
processData:
handler: handler.process
timeout: 30 # Maximum 30 seconds
memorySize: 512 # Limit memory
reservedConcurrency: 10 # Limit concurrent executions
Observability and Monitoring
Security isn’t just prevention—it’s also detection and response:
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();
async function recordSecurityEvent(eventType, metadata) {
await cloudwatch.putMetricData({
Namespace: 'SecurityEvents',
MetricData: [{
MetricName: eventType,
Value: 1,
Unit: 'Count',
Timestamp: new Date()
}]
}).promise();
console.log(JSON.stringify({
eventType,
timestamp: new Date().toISOString(),
...metadata
}));
}
exports.handler = async (event) => {
try {
// Validate authorization
if (!isAuthorized(event)) {
await recordSecurityEvent('UnauthorizedAccess', {
sourceIp: event.requestContext.identity.sourceIp,
path: event.path
});
return {
statusCode: 403,
body: JSON.stringify({ error: 'Forbidden' })
};
}
// Process request
} catch (err) {
await recordSecurityEvent('FunctionError', {
error: err.message
});
throw err;
}
};
Cold Start Security Implications
Cold starts introduce unique security considerations. Initialization code runs on every new container instance, which is an opportunity for:
- Fetching and caching secrets securely
- Validating runtime environment
- Initializing security libraries
But it’s also a vulnerability window where timing attacks might leak information about your infrastructure.
Event Source Security
Different event sources have different security models:
API Gateway: Implement authentication (API keys, JWT validation, custom authorizers)
S3 Events: Validate event structure and source bucket
SNS/SQS: Verify message signatures
CloudWatch Events: Ensure events match expected patterns
Example custom authorizer:
exports.authorizer = async (event) => {
const token = event.authorizationToken;
try {
const decoded = verifyJWT(token);
return {
principalId: decoded.sub,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: 'Allow',
Resource: event.methodArn
}]
},
context: {
userId: decoded.sub,
email: decoded.email
}
};
} catch (err) {
throw new Error('Unauthorized');
}
};
Compliance and Audit
Serverless doesn’t exempt you from compliance requirements:
- Logging: Ensure comprehensive logging of all function invocations and data access
- Audit Trails: Use CloudTrail or equivalent to track API calls
- Data Residency: Understand where your functions execute and data is stored
- Data Retention: Implement appropriate retention policies
Conclusion
Serverless security requires a shift in mindset from infrastructure-centric to code-centric security. The abstraction of infrastructure management doesn’t reduce security responsibilities—it refocuses them on application logic, dependencies, and configuration.
Key takeaways:
- Apply least privilege rigorously at the function level
- Treat every function input as untrusted
- Use managed services for secrets and encryption
- Monitor and audit comprehensively
- Keep dependencies minimal and updated
- Validate and sanitize all inputs
As serverless adoption continues to grow, security practices will mature. The teams that succeed will be those that embrace the paradigm shift and build security into their development workflows from day one.