ComputeIntermediate18 min read

    AWS Lambda Security Best Practices

    Tarek Cheikh

    Founder & AWS Security Expert

    View Security Card

    AWS Lambda is the backbone of modern serverless architectures, executing billions of invocations daily across AWS customers. Because Lambda abstracts away the underlying infrastructure, teams often assume security is "handled." In reality, Lambda shifts the security responsibility -- you no longer patch operating systems, but you are fully responsible for function code, execution roles, network configuration, secrets handling, and access policies.

    In November 2025, researchers documented an AI-accelerated breach campaign where attackers used large language models to analyze stolen AWS credentials and automatically craft Lambda function code injection payloads, deploying crypto miners across multiple accounts in under 10 minutes. Separately, Wiz Research identified thousands of Lambda Function URLs exposed to the internet with AuthType: NONE, creating unauthenticated entry points into private cloud environments. These incidents underscore that Lambda security requires deliberate, layered controls.

    This guide covers 12 battle-tested Lambda security best practices, each with real AWS CLI commands, audit procedures, and the latest 2025-2026 updates including re:Invent 2025 announcements like Durable Functions and Tenant Isolation improvements.

    1. Execution Role Least Privilege

    Every Lambda function assumes an IAM execution role that defines what AWS services and resources it can access. The most common mistake is sharing a single overly permissive role across multiple functions. If any one function is compromised, the attacker inherits all permissions granted to that shared role.

    Implementation

    • One role per function. Each function should have a dedicated execution role scoped to the exact resources it needs. A function that reads from a single DynamoDB table should not have dynamodb:* on *.
    • Use IAM Access Analyzer. Generate least-privilege policies based on actual CloudTrail usage rather than guessing required permissions.
    • Apply permission boundaries. Set a maximum permission ceiling so even if a developer attaches broad policies, the effective permissions remain constrained.
    # Create a scoped execution role for a single function
    aws iam create-role   --role-name order-processor-lambda-role   --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Principal": {"Service": "lambda.amazonaws.com"},
          "Action": "sts:AssumeRole"
        }]
      }'
    
    # Attach only the permissions this specific function needs
    aws iam put-role-policy   --role-name order-processor-lambda-role   --policy-name order-processor-policy   --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
            "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Orders"
          },
          {
            "Effect": "Allow",
            "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
            "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/order-processor:*"
          }
        ]
      }'
    
    # Generate a least-privilege policy from CloudTrail activity
    aws accessanalyzer start-policy-generation   --policy-generation-details '{"principalArn":"arn:aws:iam::123456789012:role/order-processor-lambda-role"}'   --cloud-trail-details '{"trails":[{"cloudTrailArn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/my-trail","allRegions":true}],"accessRole":"arn:aws:iam::123456789012:role/AccessAnalyzerRole","startTime":"2025-09-01T00:00:00Z","endTime":"2026-03-01T00:00:00Z"}'

    CIS Benchmark: Control 2.1.2 (Lambda functions should not have overly permissive execution roles). AWS Security Hub evaluates this automatically when the AWS Foundational Security Best Practices standard is enabled.

    2. Encrypt Environment Variables with KMS

    Lambda environment variables are encrypted at rest by default using an AWS-managed KMS key. However, anyone with lambda:GetFunction permission can retrieve the decrypted values. For sensitive configuration, this default encryption is insufficient.

    Implementation

    • Use a customer-managed KMS key (CMK) for encryption at rest. This lets you control key policies and audit decryption via CloudTrail.
    • Enable encryption helpers for encryption in transit. This encrypts individual environment variable values client-side before Lambda stores them, requiring an explicit kms:Decrypt call in your function code.
    • Prefer Secrets Manager over environment variables for truly sensitive data like database passwords, API keys, and tokens (see Practice 11).
    # Create a customer-managed KMS key for Lambda
    aws kms create-key   --description "Lambda environment variable encryption"   --key-usage ENCRYPT_DECRYPT
    
    # Update function to use the CMK for at-rest encryption
    aws lambda update-function-configuration   --function-name order-processor   --kms-key-arn arn:aws:kms:us-east-1:123456789012:key/abcd1234-ef56-gh78-ij90-klmnopqrstuv
    
    # Audit: list functions and their KMS key configuration
    aws lambda list-functions   --query "Functions[?KMSKeyArn==null].[FunctionName]"   --output table

    Caveat: Environment variables are limited to 4 KB total. For larger configuration payloads, use AWS Systems Manager Parameter Store or Secrets Manager.

    3. VPC Placement for Private Resource Access

    By default, Lambda functions run in an AWS-managed VPC with internet access but no access to your private VPC resources (RDS instances, ElastiCache clusters, internal APIs). If your function needs to reach private resources, you must configure VPC placement.

    Implementation

    • Place functions in private subnets with no direct internet route. Use a NAT gateway only if the function also needs outbound internet access.
    • Use VPC endpoints for AWS service access (S3, DynamoDB, SQS, Secrets Manager) to keep traffic on the AWS network and avoid NAT gateway costs.
    • Assign dedicated security groups that restrict outbound traffic to only the required destinations and ports.
    # Create a dedicated security group for the Lambda function
    aws ec2 create-security-group   --group-name lambda-order-processor-sg   --description "Security group for order-processor Lambda"   --vpc-id vpc-0abc123def456
    
    # Allow outbound access only to the RDS instance on port 5432
    aws ec2 authorize-security-group-egress   --group-id sg-0abc123   --protocol tcp   --port 5432   --source-group sg-0rds456
    
    # Configure the Lambda function for VPC access
    aws lambda update-function-configuration   --function-name order-processor   --vpc-config SubnetIds=subnet-0priv1,subnet-0priv2,SecurityGroupIds=sg-0abc123
    
    # Create a VPC endpoint for Secrets Manager (avoid NAT for AWS services)
    aws ec2 create-vpc-endpoint   --vpc-id vpc-0abc123def456   --service-name com.amazonaws.us-east-1.secretsmanager   --vpc-endpoint-type Interface   --subnet-ids subnet-0priv1 subnet-0priv2   --security-group-ids sg-0vpce789

    Performance note: Since the 2019 Hyperplane ENI improvements, VPC-configured Lambda functions no longer suffer cold start penalties for ENI attachment. ENIs are pre-created and shared across functions in the same security group and subnet combination.

    4. Function URL Authentication

    Lambda Function URLs provide a dedicated HTTPS endpoint for invoking a function without API Gateway. They support two authentication modes: AWS_IAM and NONE. Wiz Research found thousands of Function URLs set to NONE, exposing internal functions to the public internet without any authentication.

    Implementation

    • Always use AWS_IAM authentication. The NONE mode should be reserved for genuinely public endpoints (rare) and should be paired with application-level authentication.
    • Use SCP to block unauthenticated Function URLs across your organization.
    • Understand the October 2025 dual-permission model. Function URL invocations now require both lambda:InvokeFunctionUrl and a resource-based policy allowing the caller. This dual-permission check prevents accidental exposure.
    # Create a Function URL with IAM authentication (correct)
    aws lambda create-function-url-config   --function-name order-api   --auth-type AWS_IAM   --cors '{"AllowOrigins":["https://app.example.com"],"AllowMethods":["GET","POST"]}'
    
    # Audit: find functions with NONE authentication (security risk)
    aws lambda list-functions --query "Functions[].FunctionName" --output text |   tr '	' '
    ' | while read fn; do
        auth=$(aws lambda get-function-url-config --function-name "$fn"       --query "AuthType" --output text 2>/dev/null)
        if [ "$auth" = "NONE" ]; then
          echo "WARNING: $fn has unauthenticated Function URL"
        fi
      done

    SCP to Prevent Unauthenticated Function URLs

    {
      "Version": "2012-10-17",
      "Statement": [{
        "Sid": "DenyUnauthenticatedFunctionURLs",
        "Effect": "Deny",
        "Action": "lambda:CreateFunctionUrlConfig",
        "Resource": "*",
        "Condition": {
          "StringEquals": {
            "lambda:FunctionUrlAuthType": "NONE"
          }
        }
      }]
    }

    2025 Update: The October 2025 dual-permission model for Function URLs adds a second authorization check. New functions automatically require both IAM principal permissions and a resource-based policy grant. Existing functions created before October 2025 should be reviewed and migrated to the new model.

    5. Audit Resource-Based Policies

    Lambda resource-based policies control which AWS principals and services can invoke a function. An overly permissive resource-based policy -- especially one with Principal: "*" -- can allow anyone on the internet (or any AWS account) to invoke your function.

    Implementation

    • Never use Principal: "*" without a restrictive condition key such as aws:PrincipalOrgID or aws:SourceArn.
    • Use IAM Access Analyzer to detect resource-based policies that grant external access.
    • Regularly audit function policies for unintended cross-account access.
    # Review the resource-based policy on a function
    aws lambda get-policy --function-name order-processor   --query "Policy" --output text | python3 -m json.tool
    
    # Audit all functions for overly permissive policies
    aws lambda list-functions --query "Functions[].FunctionName" --output text |   tr '	' '
    ' | while read fn; do
        policy=$(aws lambda get-policy --function-name "$fn"       --query "Policy" --output text 2>/dev/null)
        if echo "$policy" | grep -q '"Principal":"*"'; then
          echo "CRITICAL: $fn has Principal: * in resource-based policy"
        fi
      done
    
    # Remove an overly permissive statement
    aws lambda remove-permission   --function-name order-processor   --statement-id overly-permissive-statement

    CIS Benchmark: Ensure Lambda function policies do not grant public access. AWS Security Hub flags this under the AWS Foundational Security Best Practices standard (Lambda.1).

    6. Secure Lambda Layers

    Lambda layers provide shared libraries, runtimes, and dependencies across functions. A malicious or compromised layer can inject code into every function that uses it. Supply chain attacks through layers are a growing concern.

    Implementation

    • Only use layers from trusted sources. Prefer layers published by AWS, your organization, or verified partners.
    • Pin layer versions. Always reference a specific layer version ARN, never the unversioned ARN which resolves to the latest version and could change unexpectedly.
    • Restrict layer sharing. Use lambda:AddLayerVersionPermission carefully, and audit who has access to publish new layer versions.
    • Scan layer contents. Treat layers as dependencies -- run SAST/SCA scans on layer code and libraries.
    # List layers and their permissions
    aws lambda list-layers --query "Layers[].{Name:LayerName,ARN:LatestMatchingVersion.LayerVersionArn}"
    
    # Check permissions on a specific layer version
    aws lambda get-layer-version-policy   --layer-name my-shared-lib   --version-number 3
    
    # Pin a specific layer version (correct)
    aws lambda update-function-configuration   --function-name order-processor   --layers arn:aws:lambda:us-east-1:123456789012:layer:my-shared-lib:3
    
    # Restrict layer sharing to your organization only
    aws lambda add-layer-version-permission   --layer-name my-shared-lib   --version-number 3   --statement-id org-access   --action lambda:GetLayerVersion   --principal "*"   --organization-id o-abc123def4

    7. Enable Code Signing with AWS Signer

    Code signing ensures that only trusted, unmodified code can be deployed to your Lambda functions. AWS Signer creates a cryptographic signature of your deployment package, and Lambda verifies this signature before allowing the code to run.

    Implementation

    • Create a signing profile with AWS Signer for your organization.
    • Create a code signing configuration that specifies allowed signing profiles and the enforcement action (Warn or Enforce).
    • Set the policy to Enforce in production to block deployment of unsigned or tampered code.
    # Create a signing profile (valid for 3 years)
    aws signer put-signing-profile   --profile-name LambdaProductionSigning   --platform-id AWSLambda-SHA384-ECDSA   --signature-validity-period value=135,type=MONTHS
    
    # Create a code signing configuration
    aws lambda create-code-signing-config   --allowed-publishers SigningProfileVersionArns=arn:aws:signer:us-east-1:123456789012:/signing-profiles/LambdaProductionSigning   --code-signing-policies UntrustedArtifactOnDeployment=Enforce   --description "Production Lambda code signing"
    
    # Attach the code signing config to a function
    aws lambda update-function-code-signing-config   --function-name order-processor   --code-signing-config-arn arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-abc123
    
    # Sign a deployment package
    aws signer start-signing-job   --source 's3={bucketName=my-deployment-bucket,key=order-processor.zip,version=abc123}'   --destination 's3={bucketName=my-signed-bucket,prefix=signed/}'   --profile-name LambdaProductionSigning

    Best practice: Integrate code signing into your CI/CD pipeline. The build step creates the deployment package, the signing step signs it with AWS Signer, and the deployment step uses the signed artifact. Any tampering between build and deployment is caught.

    8. Reserved Concurrency for DoS Protection

    Without concurrency limits, a Lambda function can scale to consume your entire account's concurrency pool (default 1,000 concurrent executions per region). An attacker flooding one function with requests can starve all other functions in the account -- a form of denial-of-service.

    Implementation

    • Set reserved concurrency on each function to cap its maximum concurrent executions.
    • Set reserved concurrency to 0 to immediately disable a compromised function without deleting it.
    • Use provisioned concurrency for latency-critical functions that also need protection, as it both reserves capacity and eliminates cold starts.
    # Set reserved concurrency to limit blast radius
    aws lambda put-function-concurrency   --function-name order-processor   --reserved-concurrent-executions 100
    
    # Emergency: disable a compromised function immediately
    aws lambda put-function-concurrency   --function-name compromised-function   --reserved-concurrent-executions 0
    
    # Audit: list functions without reserved concurrency
    aws lambda list-functions --query "Functions[].FunctionName" --output text |   tr '	' '
    ' | while read fn; do
        conc=$(aws lambda get-function-concurrency --function-name "$fn"       --query "ReservedConcurrentExecutions" --output text 2>/dev/null)
        if [ "$conc" = "None" ]; then
          echo "WARNING: $fn has no reserved concurrency"
        fi
      done

    Best practice: Leave at least 10% of your account concurrency unreserved so new or unscoped functions can still execute. Monitor the ConcurrentExecutions and Throttles CloudWatch metrics to right-size your concurrency limits.

    9. Configure Dead Letter Queues for Failure Visibility

    When an asynchronous Lambda invocation fails after all retries, the event is silently dropped unless you configure a Dead Letter Queue (DLQ). Lost events mean lost visibility into failures, which can mask security-relevant errors such as permission denials, injection attempts, or data exfiltration failures.

    Implementation

    • Configure a DLQ (SQS queue or SNS topic) on every asynchronously invoked function.
    • Prefer Lambda Destinations over DLQs for richer failure context -- Destinations include the full error message, stack trace, and request payload.
    • Set up CloudWatch alarms on the DLQ message count to alert on failures.
    # Create an SQS dead letter queue
    aws sqs create-queue --queue-name order-processor-dlq
    
    # Configure the DLQ on the Lambda function
    aws lambda update-function-configuration   --function-name order-processor   --dead-letter-config TargetArn=arn:aws:sqs:us-east-1:123456789012:order-processor-dlq
    
    # Better: use Lambda Destinations for richer failure context
    aws lambda put-function-event-invoke-config   --function-name order-processor   --destination-config '{
        "OnFailure": {
          "Destination": "arn:aws:sqs:us-east-1:123456789012:order-processor-failures"
        }
      }'
    
    # Create a CloudWatch alarm on the DLQ
    aws cloudwatch put-metric-alarm   --alarm-name order-processor-dlq-alarm   --namespace AWS/SQS   --metric-name ApproximateNumberOfMessagesVisible   --dimensions Name=QueueName,Value=order-processor-dlq   --statistic Sum   --period 300   --threshold 1   --comparison-operator GreaterThanOrEqualToThreshold   --evaluation-periods 1   --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts

    Best practice: The execution role must have sqs:SendMessage or sns:Publish permission on the DLQ resource. A common misconfiguration is setting up the DLQ but forgetting the IAM permission, causing silent failures.

    10. Manage Runtime Updates

    Lambda managed runtimes receive automatic security patches from AWS. How and when those patches apply depends on your runtime update configuration. Using outdated or end-of-life runtimes means your functions run on unpatched environments with known vulnerabilities.

    Implementation

    • Use Auto mode (recommended). AWS applies runtime patches automatically within the first few days of release. This is the default and the most secure option for most workloads.
    • Use Function Update mode only if you need to test patches before they apply. Patches apply only when you next update the function configuration or code.
    • Never use deprecated runtimes. AWS blocks function creation on deprecated runtimes and eventually blocks updates. Migrate proactively.
    # Check the runtime management configuration
    aws lambda get-runtime-management-config   --function-name order-processor
    
    # Set to Auto mode (recommended)
    aws lambda put-runtime-management-config   --function-name order-processor   --update-runtime-on Auto
    
    # Audit: find functions using deprecated runtimes
    aws lambda list-functions   --query "Functions[?Runtime=='python3.8' || Runtime=='nodejs16.x' || Runtime=='dotnet6'].[FunctionName,Runtime]"   --output table

    CIS Benchmark: Ensure Lambda functions use supported runtimes. AWS Security Hub flags functions using deprecated runtimes under the AWS Foundational Security Best Practices standard (Lambda.2).

    11. Use Secrets Manager Instead of Environment Variables

    Storing secrets in Lambda environment variables -- even encrypted with KMS -- exposes them to anyone with lambda:GetFunction or lambda:GetFunctionConfiguration permissions. Secrets Manager provides automatic rotation, fine-grained access control, and audit logging for every secret retrieval.

    Implementation

    • Store all secrets in Secrets Manager and retrieve them at function startup or on each invocation.
    • Use the Lambda Secrets Manager extension (AWS Parameters and Secrets Lambda Extension) to cache secrets in memory and reduce API calls.
    • Enable automatic rotation for database credentials and API keys.
    • Use Powertools for AWS Lambda parameters utility for clean, cached secret retrieval with built-in error handling.
    # Create a secret in Secrets Manager
    aws secretsmanager create-secret   --name prod/order-processor/db-credentials   --secret-string '{"username":"app_user","password":"REPLACE_ME"}'   --kms-key-id alias/lambda-secrets-key
    
    # Add the Secrets Manager extension layer to your function
    aws lambda update-function-configuration   --function-name order-processor   --layers arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:12
    
    # Grant the execution role permission to read the secret
    aws iam put-role-policy   --role-name order-processor-lambda-role   --policy-name secrets-access   --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Action": ["secretsmanager:GetSecretValue"],
          "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/order-processor/db-credentials-*"
        }]
      }'
    
    # Enable automatic rotation (every 30 days)
    aws secretsmanager rotate-secret   --secret-id prod/order-processor/db-credentials   --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:secret-rotation   --rotation-rules AutomaticallyAfterDays=30

    Powertools for AWS Lambda: The Powertools library (available for Python, TypeScript, Java, and .NET) provides a parameters utility that integrates with Secrets Manager and Parameter Store. It handles caching, decryption, and error handling out of the box. Additionally, Powertools offers data masking utilities that automatically redact sensitive fields from logs, and idempotency utilities that prevent duplicate processing of the same event -- both critical for security.

    12. SnapStart Security Considerations

    Lambda SnapStart (available for Java and Python) dramatically reduces cold start latency by creating a snapshot of the initialized function and reusing it across invocations. However, this creates two security hazards that must be addressed.

    Uniqueness Hazard

    Any random values, unique identifiers, or cryptographic seeds generated during initialization are captured in the snapshot and reused identically across all invocations. This can lead to predictable UUIDs, repeated encryption nonces, and duplicate session tokens.

    Secrets Freshness Hazard

    Secrets or credentials retrieved during initialization are frozen in the snapshot. If a secret is rotated after the snapshot is taken, the function continues using the stale credential until the snapshot is refreshed.

    Implementation

    • Move secret retrieval to the invocation phase (inside the handler) or use a runtime hook to refresh secrets after snapshot restoration.
    • Use CRaC (Coordinated Restore at Checkpoint) hooks for Java to re-initialize random number generators and refresh connections after restore.
    • Never generate cryptographic material during init when SnapStart is enabled. Generate nonces, IVs, and session tokens inside the handler.
    # Enable SnapStart on a Java function
    aws lambda update-function-configuration   --function-name order-processor   --snap-start ApplyOn=PublishedVersions
    
    # Publish a new version to create the snapshot
    aws lambda publish-version   --function-name order-processor
    
    # Verify SnapStart optimization status
    aws lambda get-function   --function-name order-processor   --query "Configuration.SnapStart"

    Best practice: After enabling SnapStart, audit your initialization code for any calls to SecureRandom, UUID.randomUUID(), or secret retrieval. Use the afterRestore CRaC hook to re-initialize these after snapshot restoration. For Python SnapStart, move all secret fetches and random seed generation into the handler function.


    re:Invent 2025 Lambda Announcements

    AWS re:Invent 2025 introduced several Lambda features with security implications:

    • Durable Functions (GA for Python/Node.js; Java in Preview): Long-running Lambda workflows with built-in checkpointing and state persistence, launched at re:Invent 2025. Security consideration: checkpoint state is stored in an AWS-managed durable store. Ensure your execution role follows least privilege, and be aware that function state (including any in-memory secrets) may be serialized to the checkpoint.
    • Tenant Isolation Improvements: Enhanced support for multi-tenant serverless applications with per-tenant execution role assumption and fine-grained resource-level authorization. This enables the SaaS best practice of dynamic role assumption per tenant rather than a shared execution role.
    • Python SnapStart (GA): SnapStart support expanded beyond Java to Python runtimes, bringing the same cold start improvements and the same uniqueness and secrets freshness hazards discussed in Practice 12.

    Common Misconfigurations

    Misconfiguration Risk Detection
    Shared execution role across multiple functions One compromised function exposes all permissions Audit: multiple functions referencing the same Role ARN
    Function URL with AuthType: NONE Unauthenticated public access to function AWS Config: lambda-function-url-auth-type-check
    Principal: "*" in resource-based policy Any AWS account can invoke the function IAM Access Analyzer external access findings
    Secrets stored in environment variables Exposed via lambda:GetFunctionConfiguration Code review; scan for plaintext secrets in env vars
    No reserved concurrency configured DoS can exhaust account-wide concurrency Audit: GetFunctionConcurrency returns null
    Deprecated runtime (e.g., Python 3.8, Node.js 16) Unpatched vulnerabilities in runtime Security Hub: Lambda.2 finding
    VPC function with overly permissive security group Unrestricted network access from function Security group audit: 0.0.0.0/0 egress rules
    SnapStart with secrets fetched during init Stale credentials after secret rotation Code review for init-phase secret retrieval

    Quick Reference Checklist

    # Practice Priority
    1Execution role least privilege (one role per function)Critical
    2Encrypt environment variables with CMKHigh
    3VPC placement for private resource accessHigh
    4Function URL authentication (always AWS_IAM)Critical
    5Audit resource-based policies (no Principal: *)Critical
    6Secure Lambda layers (trusted sources, pin versions)High
    7Enable code signing with AWS SignerHigh
    8Reserved concurrency for DoS protectionHigh
    9Configure dead letter queues for failure visibilityMedium
    10Manage runtime updates (Auto mode)High
    11Use Secrets Manager instead of environment variablesCritical
    12SnapStart security (uniqueness and secrets freshness)Medium

    Related Resources

    Go Deeper: The State of AWS Security 2026

    This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.

    LambdaServerlessExecution RoleFunction URLCode SigningVPCSecrets Manager