Serverless Security — IAM, Least Privilege, and Best Practices
In this tutorial, you will learn about Serverless Security. We cover key concepts, practical examples, and best practices to help you master this topic.
Serverless security focuses on IAM policies, function permissions, secret management, input validation, and understanding the shared responsibility model where AWS manages the infrastructure and you secure your code and data.
What You'll Learn
By the end of this lesson you will understand IAM roles for Lambda, Least Privilege principles, managing secrets, securing API endpoints, VPC security, and serverless-specific security threats.
Why It Matters
Serverless reduces infrastructure attack surface but introduces new security concerns. Overly permissive IAM roles, exposed secrets in environment variables, and unvalidated event inputs are common vulnerabilities that can lead to data breaches.
Real-World Use
DodaTech's serverless applications follow strict security policies: each function has minimal IAM permissions scoped to specific resources, secrets are retrieved from AWS Secrets Manager at initialization, and all event inputs are validated with JSON Schema.
flowchart TD
subgraph "Security Layers"
IAM[IAM Roles] -->|Least Privilege| F[Lambda Function]
SM[Secrets Manager] -->|Encrypted| F
IV[Input Validation] -->|Sanitize| F
VPC[VPC Security] -->|Network Isolation| F
end
F --> A[AWS Services]
F --> B[External APIs]
style F fill:#f90,color:#fff
IAM Least Privilege
Each function should have only the permissions it needs. Never use wildcard resources or actions.
# iam_policies.py
# IAM least privilege patterns
def example_overly_permissive():
policy = {
"Effect": "Allow",
"Action": ["dynamodb:*"],
"Resource": "*"
}
print("BAD: Full DynamoDB access to all tables")
print(json.dumps(policy, indent=2))
def example_least_privilege():
policy = {
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/users-table"
}
print("GOOD: Specific actions on specific table")
print(json.dumps(policy, indent=2))
import json
example_overly_permissive()
print()
example_least_privilege()
Secret Management
Never store secrets in code or environment variables visible in the console. Use AWS Secrets Manager with IAM controls.
# secrets.py
# Secure secret management
import json
import os
class SecretManager:
def __init__(self):
self.cache = {}
def get_secret(self, secret_id):
if secret_id in self.cache:
print(f"Cache HIT for {secret_id}")
return self.cache[secret_id]
print(f"Retrieving {secret_id} from Secrets Manager...")
secret = {"api_key": "sk_live_abc123", "db_password": "encrypted-value"}
self.cache[secret_id] = secret
return secret
def lambda_handler(event, context):
secrets = SecretManager()
stripe_key = secrets.get_secret("prod/stripe/key")["api_key"]
db_password = secrets.get_secret("prod/db/password")["db_password"]
print(f"Using Stripe key: {stripe_key[:6]}...")
print(f"Using DB password: {'*' * 8}")
return {"statusCode": 200, "body": json.dumps({"configured": True})}
print(lambda_handler({}, None)["body"])
Input Validation
Validate and sanitize all event inputs to prevent injection attacks.
# input_validation.py
# Event input validation
import json
import re
def validate_create_user_input(body):
errors = []
if not isinstance(body, dict):
return {"valid": False, "errors": ["Body must be a JSON object"]}
if "email" not in body:
errors.append("email is required")
elif not re.match(r"[^@]+@[^@]+\.[^@]+", body["email"]):
errors.append("Invalid email format")
if "name" not in body:
errors.append("name is required")
elif len(body["name"]) > 100:
errors.append("name exceeds 100 characters")
if "role" in body and body["role"] not in ["admin", "user", "viewer"]:
errors.append("Invalid role")
return {"valid": len(errors) == 0, "errors": errors}
def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))
validation = validate_create_user_input(body)
if not validation["valid"]:
return {"statusCode": 400, "body": json.dumps(validation)}
return {"statusCode": 201, "body": json.dumps({"created": body["email"]})}
test_cases = [
'{}',
'{"email": "invalid", "name": "Alice"}',
'{"email": "alice@example.com", "name": "Alice", "role": "admin"}',
]
for case in test_cases:
result = lambda_handler({"body": case}, None)
print(f"Input: {case}")
print(f" Response: {result['statusCode']} - {result['body']}")
Common Mistakes
Overly permissive IAM roles: Using
Action: ["*"]orResource: "*"grants access to all resources. Scope permissions tightly.Storing secrets in environment variables: Environment variables are visible in the console. Use Secrets Manager or Parameter Store.
Not validating event inputs: Unvalidated inputs can cause injection attacks in DynamoDB, SQL, or command execution.
Allowing wide API Gateway CORS without origin validation: Returning
Access-Control-Allow-Origin: *allows any website to make requests.Ignoring the principle of least privilege for VPC: Functions in a VPC should have minimal network access through security groups and NACLs.
Practice Questions
What is the principle of least privilege? Granting only the minimum permissions needed for a function to perform its specific task.
How should you store API keys for Lambda functions? Use AWS Secrets Manager with IAM policies restricting access to specific functions.
Why is input validation important in serverless? Unvalidated inputs can lead to injection attacks, data corruption, and unauthorized access.
What is the shared responsibility model in serverless? AWS secures the infrastructure. You secure your code, data, IAM policies, and secrets.
Challenge: Create IAM policies for a Lambda function that needs to read from S3, write to DynamoDB, and send SNS notifications, following least privilege.
FAQ
Mini Project
Create a secure serverless function that validates input, uses Secrets Manager for credentials, applies least privilege IAM, and logs security events.
import json
import re
import os
VALID_ROLES = {"admin", "user", "viewer"}
def validate_user(data):
if not data.get("email") or not re.match(r"[^@]+@[^@]+\.[^@]+", data["email"]):
return False, "Invalid email"
if not data.get("role") or data["role"] not in VALID_ROLES:
return False, "Invalid role"
return True, ""
def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))
valid, error = validate_user(body)
if not valid:
return {"statusCode": 400, "body": json.dumps({"error": error})}
# Retrieve secrets
print("[Security] Retrieved database password from Secrets Manager")
print(f"[Security] Creating user: {body['email']} with role: {body['role']}")
return {"statusCode": 201, "body": json.dumps({"email": body["email"], "role": body["role"]})}
print(lambda_handler({"body": json.dumps({"email": "alice@example.com", "role": "admin"})}, None)["body"])
What's Next
Next: Serverless Best Practices for production readiness.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro