← Back to Blog

AWS Lambda Security Hardening: A Complete Guide to Securing Serverless Functions

5 min read

AWS Lambda's managed execution model eliminates server patching, but it does not eliminate the need for security hardening. Misconfigured Lambda functions are a common source of privilege escalation, data leakage, and lateral movement in cloud environments. This guide covers the essential security controls for production Lambda deployments.

Least-Privilege Execution Roles

Every Lambda function assumes an IAM execution role at runtime. The most common mistake is sharing a single overpermissive role across multiple functions.

Dedicated Per-Function Roles

import boto3
import json

def create_lambda_execution_role(function_name, allowed_actions):
    """Create a dedicated least-privilege execution role for a Lambda function"""

    iam = boto3.client('iam')

    assume_role_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
                "Condition": {
                    "StringEquals": {
                        "aws:SourceAccount": boto3.client('sts').get_caller_identity()['Account']
                    }
                }
            }
        ]
    }

    role = iam.create_role(
        RoleName=f"lambda-{function_name}-role",
        AssumeRolePolicyDocument=json.dumps(assume_role_policy),
        Description=f"Execution role for {function_name} Lambda function"
    )

    # Attach only the specific permissions needed
    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ],
                "Resource": f"arn:aws:logs:*:*:log-group:/aws/lambda/{function_name}:*"
            },
            {
                "Effect": "Allow",
                "Action": allowed_actions,
                "Resource": "*"
            }
        ]
    }

    iam.put_role_policy(
        RoleName=role['Role']['RoleName'],
        PolicyName=f"{function_name}-permissions",
        PolicyDocument=json.dumps(policy_document)
    )

    return role['Role']['Arn']

Key principles:

  • One role per function — never share roles across functions with different responsibilities
  • Scope log permissions — restrict logs:* to the specific log group for the function
  • Use resource-level constraints — replace "Resource": "*" with specific ARNs wherever possible
  • Add source account conditions — prevent confused deputy attacks on the trust policy

VPC Configuration for Private Resources

Lambda functions that access databases, caches, or internal APIs should run inside a VPC. Functions that only call public AWS APIs generally should not — VPC placement adds cold start latency and requires NAT Gateway for internet access.

Secure VPC Lambda Configuration

# serverless.yml or SAM template excerpt
Resources:
  SecureFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: secure-data-processor
      Runtime: python3.12
      Handler: handler.main
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref PrivateSubnet1
          - !Ref PrivateSubnet2
      # Reserved concurrency prevents runaway invocations
      ReservedConcurrentExecutions: 100

  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Lambda function security group
      VpcId: !Ref VPC
      SecurityGroupEgress:
        # Only allow outbound to the database
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          DestinationSecurityGroupId: !Ref DatabaseSecurityGroup
        # Allow outbound to VPC endpoints (HTTPS)
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          DestinationPrefixListId: !Ref S3PrefixList

  # VPC endpoint for DynamoDB access without NAT
  DynamoDBEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTable

Best practices for VPC Lambda:

  • Place functions in private subnets only — never in public subnets
  • Use VPC endpoints for DynamoDB, S3, SQS, and other AWS services to avoid NAT costs
  • Restrict security group egress rules to specific destinations and ports
  • Deploy across multiple AZs for high availability

Environment Variable Encryption

Lambda environment variables are encrypted at rest by default with an AWS-managed key. For sensitive values, use a customer-managed KMS key and decrypt at runtime.

Runtime Decryption Pattern

import boto3
import os
import base64
from functools import lru_cache

kms_client = boto3.client('kms')

@lru_cache(maxsize=16)
def decrypt_env_var(encrypted_value):
    """Decrypt a KMS-encrypted environment variable with caching"""
    decrypted = kms_client.decrypt(
        CiphertextBlob=base64.b64decode(encrypted_value),
        EncryptionContext={
            'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']
        }
    )
    return decrypted['Plaintext'].decode('utf-8')

def handler(event, context):
    # Decrypt sensitive values — cached across warm invocations
    db_password = decrypt_env_var(os.environ['ENCRYPTED_DB_PASSWORD'])
    api_key = decrypt_env_var(os.environ['ENCRYPTED_API_KEY'])

    # Use decrypted values
    return process_event(event, db_password, api_key)

The @lru_cache decorator ensures KMS is only called once per cold start, keeping costs low and latency minimal.

Resource-Based Policies

Lambda resource-based policies control who can invoke a function. Without explicit restrictions, any principal with lambda:InvokeFunction permission on the function ARN can call it.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAPIGatewayInvoke",
      "Effect": "Allow",
      "Principal": {"Service": "apigateway.amazonaws.com"},
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-api-handler",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:us-east-1:123456789012:abc123def4/*"
        }
      }
    }
  ]
}

Always include a Condition block that restricts invocation to the expected source. Without it, any API Gateway in the account — or any account, if the principal is a wildcard — can invoke the function.

Monitoring and Threat Detection

Enable GuardDuty Lambda Protection to detect:

  • Functions communicating with known command-and-control servers
  • Cryptocurrency mining activity
  • DNS queries to malicious domains from VPC-attached functions

Use CloudWatch Logs Insights to detect anomalous patterns:

fields @timestamp, @message
| filter @message like /AccessDenied/
| stats count() as denied_count by bin(1h) as hour
| sort denied_count desc

Frequent AccessDenied errors may indicate a compromised function probing for permissions beyond its role.

Securing Lambda Functions with AccessLens

Lambda execution roles are one of the most common sources of IAM privilege escalation. An overpermissive Lambda role combined with code injection can give an attacker broad access to your AWS environment.

AccessLens helps secure your Lambda deployment by providing:

  • Execution role analysis that identifies overpermissive Lambda roles and unused permissions
  • Cross-service access mapping that shows which resources each Lambda function can reach
  • Privilege escalation detection that finds Lambda roles capable of modifying IAM policies or assuming other roles
  • Trust relationship visualization that reveals unexpected invocation paths between services and accounts

Your Lambda functions are only as secure as the IAM roles they assume. AccessLens gives you the visibility to enforce true least privilege across your serverless workloads.

Secure your Lambda functions with AccessLens and eliminate the IAM blind spots in your serverless architecture.

Ready to secure your AWS environment?

Get comprehensive IAM visibility across all your AWS accounts in minutes.