AWS Security11 min read

    From Findings to Fixed: Lambda Compliance Mapping and Remediation

    Tarek Cheikh

    Founder & AWS Cloud Architect

    From findings to fixed: Lambda compliance mapping and remediation

    Part 3 of 4 in the Lambda Security Series

    Part 1 described the risks. Part 2 introduced lambda-security-scanner and the nineteen checks it runs against every function. This part closes the loop. A finding is only useful if it leads somewhere, and it usually needs to lead to two places: a compliance control that an auditor cares about, and a command that an engineer can run to make the finding go away. The scanner produces both.

    Why Compliance Maps to Serverless at All

    Compliance frameworks were mostly written before serverless existed, so people assume they do not apply. They do. The controls are about outcomes, not implementation. "Restrict network access between trusted and untrusted zones," "authenticate access to system components," "enforce least privilege," and "retain audit logs" are all requirements that a Lambda function either meets or violates, regardless of the fact that there is no server to point at.

    The scanner evaluates each function against ten frameworks and reports which controls pass and which fail, per function. In total it maps to 81 controls:

    FrameworkControlsFocus
    AWS Foundational Security Best Practices5Lambda-specific Security Hub controls
    CIS AWS Compute Services Benchmark8Compute service hardening
    PCI DSS v4.0.18Payment card data protection
    HIPAA Security Rule9Healthcare data security
    SOC 211Service organization controls
    ISO 27001:202211Information security management
    ISO 27017:20154Cloud-specific security controls
    ISO 27018:20195Protection of PII in the cloud
    GDPR8EU data protection
    NIST SP 800-53 Rev512Federal security controls

    One careful note on the control identifiers, because credibility depends on it. Most frameworks use their real citations: HIPAA 164.312(a)(1), ISO 27001 A.5.15, SOC 2 CC6.1, NIST AC-3, and so on. The CIS entries are different. They map to the genuine CIS AWS Compute Services Benchmark guidance for Lambda, but the CIS-Lambda.N identifiers are the scanner's own labels, not the benchmark's official recommendation numbers, which live under section 5. They are an alignment aid, not a verbatim citation. The scanner says so in its own documentation, and so do I.

    How a Single Finding Becomes Compliance Evidence

    The mapping is many-to-many. One misconfiguration usually breaks several controls at once, which is exactly why these findings matter to an audit.

    Take a public function URL with AuthType: NONE. That single finding fails an authentication control in PCI DSS, an access-restriction control in SOC 2, an access-control requirement in NIST 800-53, and the corresponding AWS FSBP Lambda control, all from one misconfiguration. Fix the one thing and several controls flip to passing together. The same is true in reverse for plaintext secrets, which touch data-protection requirements across PCI DSS, HIPAA, ISO 27018, and GDPR simultaneously.

    If you only need the posture and not the full security scan, run the compliance report on its own:

    lambda-security-scanner security --compliance-only

    That produces a per-function, per-framework breakdown of passed and failed controls, written as a JSON report on every run. It is the artifact to hand to an auditor or attach to a control review.

    Fixing Every Finding

    The rest of this article is the remediation playbook: one fix per check, with the AWS CLI command to apply it. Run these against your own functions after a scan tells you which ones need them.

    A.1: Update deprecated and blocked runtimes

    aws lambda update-function-configuration \
      --function-name my-function \
      --runtime python3.13

    As of May 2026, examples of current supported runtimes are nodejs24.x, nodejs22.x, python3.14, python3.13, java25, java21, dotnet10, dotnet8, ruby4.0, ruby3.4, and provided.al2023. Update before the runtime's block-update date arrives, not after, because a blocked runtime can no longer be updated in place and forces a more disruptive migration.

    A.2: Reduce the maximum timeout

    aws lambda update-function-configuration \
      --function-name my-function \
      --timeout 30

    Set the timeout to what the function actually needs. A 900-second timeout on a function that finishes in three seconds means a stuck or abused invocation can burn compute for fifteen minutes before Lambda stops it.

    A.3: Move secrets out of environment variables

    # Store the secret in Secrets Manager
    aws secretsmanager create-secret \
      --name my-function/db-password \
      --secret-string "MyPr0ductionP@ss!"
    
    # Replace the plaintext value with a reference
    aws lambda update-function-configuration \
      --function-name my-function \
      --environment '{"Variables":{"DB_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:123456789012:secret:my-function/db-password-AbCdEf"}}'

    Then resolve the secret at runtime with secretsmanager:GetSecretValue. The environment variable now holds a pointer, which is exactly the pattern the scanner treats as clean.

    A.4: Reduce ephemeral storage

    aws lambda update-function-configuration \
      --function-name my-function \
      --ephemeral-storage '{"Size": 512}'

    The default is 512 MB. If the function does not need more, do not allocate more. Larger scratch space increases what a compromised function can stage locally.

    A.5: Review external layers

    aws lambda get-function-configuration \
      --function-name my-function \
      --query "Layers[*].Arn"

    Any layer owned by an account that is not yours runs arbitrary code inside your function with your function's permissions. Confirm every external layer comes from a source you trust.

    A.6: Enable X-Ray tracing

    aws lambda update-function-configuration \
      --function-name my-function \
      --tracing-config Mode=Active

    A.7: Configure a dead letter queue

    aws sqs create-queue --queue-name my-function-dlq
    
    aws lambda update-function-configuration \
      --function-name my-function \
      --dead-letter-config TargetArn=arn:aws:sqs:us-east-1:123456789012:my-function-dlq

    B.1: Restrict the resource policy

    # Remove the wildcard statement
    aws lambda remove-permission \
      --function-name my-function \
      --statement-id public-access
    
    # Add a scoped permission instead
    aws lambda add-permission \
      --function-name my-function \
      --statement-id api-gateway-invoke \
      --action lambda:InvokeFunction \
      --principal apigateway.amazonaws.com \
      --source-arn arn:aws:execute-api:us-east-1:123456789012:myapi/*/GET/resource

    B.2: Secure function URLs

    # Require signed requests
    aws lambda update-function-url-config \
      --function-name my-function \
      --auth-type AWS_IAM
    
    # Or remove the function URL entirely
    aws lambda delete-function-url-config \
      --function-name my-function

    B.3: Restrict CORS origins

    aws lambda update-function-url-config \
      --function-name my-function \
      --cors '{"AllowOrigins":["https://myapp.example.com"]}'

    B.4: Scope down execution roles

    # Detach the overprivileged managed policy
    aws iam detach-role-policy \
      --role-name my-function-role \
      --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
    
    # Attach a least-privilege policy
    aws iam attach-role-policy \
      --role-name my-function-role \
      --policy-arn arn:aws:iam::123456789012:policy/my-function-least-privilege

    Use IAM Access Analyzer to generate a least-privilege policy from the function's actual CloudTrail activity, rather than guessing at the permission set.

    B.5: Give each function its own role

    Each function should assume a role scoped to only what that function needs. Sharing one role across functions means a compromise of the weakest function inherits the access of all the others.

    C.1: Attach a VPC when the function needs private resources

    aws lambda update-function-configuration \
      --function-name my-function \
      --vpc-config SubnetIds=subnet-abc123,subnet-def456,SecurityGroupIds=sg-12345678

    Not every function needs a VPC. Functions that reach databases, internal APIs, or other private resources do.

    C.2: Span multiple Availability Zones

    aws lambda update-function-configuration \
      --function-name my-function \
      --vpc-config SubnetIds=subnet-abc123,subnet-def456,SecurityGroupIds=sg-12345678

    Provide subnets in at least two AZs. A single-AZ deployment goes down with that one AZ.

    C.3: Restrict security group egress

    aws ec2 revoke-security-group-egress \
      --group-id sg-12345678 \
      --ip-permissions '[{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]'
    
    aws ec2 authorize-security-group-egress \
      --group-id sg-12345678 \
      --ip-permissions '[{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "10.0.0.0/8"}]}]'

    Replace allow-all outbound with the specific destinations the function needs. Unrestricted egress is how a compromised function exfiltrates data to anywhere.

    D.1: Set log retention

    aws logs put-retention-policy \
      --log-group-name /aws/lambda/my-function \
      --retention-in-days 90

    D.2: Set reserved concurrency

    aws lambda put-function-concurrency \
      --function-name my-function \
      --reserved-concurrent-executions 100

    This matters most for functions that are reachable from outside. Without a concurrency cap, an attacker who can invoke the function can invoke it without limit.

    E.1: Enable code signing

    aws signer put-signing-profile \
      --profile-name my-signing-profile \
      --platform-id AWSLambda-SHA384-ECDSA
    
    aws lambda create-code-signing-config \
      --allowed-publishers SigningProfileVersionArns=arn:aws:signer:us-east-1:123456789012:/signing-profiles/my-signing-profile \
      --code-signing-policies UntrustedArtifactOnDeployment=Enforce
    
    aws lambda put-function-code-signing-config \
      --function-name my-function \
      --code-signing-config-arn arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-abc123

    Set the policy to Enforce, not Warn. Warn logs an untrusted artifact and deploys it anyway.

    E.2: Add failure destinations to event source mappings

    aws lambda update-event-source-mapping \
      --uuid my-esm-uuid \
      --destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:us-east-1:123456789012:my-function-esm-dlq"}}'

    Fix Them in This Order

    If you fix everything in score order, you fix the right things first. The scanner's deductions already encode the priority:

    1. Public access. A public resource policy and a public function URL each cost 25 points. Remove wildcard principals and require authentication on function URLs. Do this today.
    2. Plaintext secrets. Worth 20 points without KMS. They are readable by anyone with a common configuration-read permission. Move them to Secrets Manager or SSM.
    3. Overprivileged execution roles. An admin-equivalent role costs 20 points; service wildcards and privilege escalation cost 10. Scope them to least privilege.
    4. Blocked runtimes at 15 points, then deprecated runtimes at 10. Blocked runtimes can no longer be patched at all. Deprecated runtimes have stopped receiving patches.
    5. CORS and shared roles, 10 points each. Restrict origins and split shared roles into per-function roles.
    6. Everything else: logging and retention, network controls, code signing, dead letter queues, tracing, and concurrency. Lower individual weight, but together they are the difference between a function you can investigate after an incident and one you cannot.

    One Layer Left

    Part 1 was the problem: serverless moved the attack surface closer to your code, and the misconfigurations are invisible until they are exploited. Part 2 was the tool: nineteen read-only checks, a score per function, and a clear list of what is wrong. This part was the payoff: every finding mapped to the compliance controls that care about it, and a command to fix each one.

    That covers the configuration layer completely, which is what lambda-security-scanner checks and what most teams get wrong first. But there is one layer no posture scanner reaches: the code that runs inside the function, the dependencies it ships, and the credentials it holds at runtime. A clean configuration scan and a vulnerable function are entirely compatible. Part 4 covers that application layer, and it is the difference between a competent Lambda security posture and a complete one.

    Sources

    The project is open source under the MIT license:

    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.

    Lambda SecurityServerlessComplianceAWS SecurityPCI DSSHIPAALambda

    Toc Consulting: AWS Security & Cloud Architecture

    Want expert help with AWS Security?

    Our team helps engineering teams secure and architect AWS the right way: assessment in week one, a prioritized action plan in week two.