Skip to content

Serverless Security — IAM, Least Privilege, and Best Practices

DodaTech Updated 2026-06-28 5 min read

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

  1. Overly permissive IAM roles: Using Action: ["*"] or Resource: "*" grants access to all resources. Scope permissions tightly.

  2. Storing secrets in environment variables: Environment variables are visible in the console. Use Secrets Manager or Parameter Store.

  3. Not validating event inputs: Unvalidated inputs can cause injection attacks in DynamoDB, SQL, or command execution.

  4. Allowing wide API Gateway CORS without origin validation: Returning Access-Control-Allow-Origin: * allows any website to make requests.

  5. Ignoring the principle of least privilege for VPC: Functions in a VPC should have minimal network access through security groups and NACLs.

Practice Questions

  1. What is the principle of least privilege? Granting only the minimum permissions needed for a function to perform its specific task.

  2. How should you store API keys for Lambda functions? Use AWS Secrets Manager with IAM policies restricting access to specific functions.

  3. Why is input validation important in serverless? Unvalidated inputs can lead to injection attacks, data corruption, and unauthorized access.

  4. What is the shared responsibility model in serverless? AWS secures the infrastructure. You secure your code, data, IAM policies, and secrets.

  5. 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

Can I use Lambda in a private subnet without internet access?

Yes. Use VPC endpoints for AWS services or a NAT gateway for internet access.

How do I protect againstLambda function injection?

Validate all event inputs, use parameterized queries, and never evaluate user input as code.

What is AWS WAF and should I use it?

WAF protects API Gateway from web exploits. Use it for production APIs to filter SQL injection and XSS.

Can I encrypt Lambda environment variables?

Yes. Use AWS KMS to encrypt environment variables at rest and in transit.

How do I audit Lambda security?

Use AWS Config rules for Lambda, CloudTrail for API calls, and IAM Access Analyzer for permission analysis.

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