Custom Domains for Lambda Function URLs: Why CloudFront Is Required

2 min readCloud Infrastructure

Lambda Function URLs come with an auto-generated domain that rejects requests with wrong Host headers. You can't point a CNAME directly to one. CloudFront is the only way to serve a Lambda Function URL under a custom domain — it rewrites the Host header to the function URL domain before forwarding.

awslambdacloudfrontroute53

Why you can't CNAME a custom domain directly to a Lambda Function URL

A Lambda Function URL looks like: https://abcdef1234567890.lambda-url.us-east-1.on.aws

If you create a Route 53 CNAME from api.example.com to this URL and visit api.example.com, the DNS resolution works — but the Lambda function returns HTTP 403 Forbidden.

The reason: Lambda Function URLs use Host-based routing. When AWS's edge infrastructure receives a request, it reads the Host header to determine which function URL to invoke. A request arriving with Host: api.example.com doesn't match any function URL mapping — only Host: abcdef1234567890.lambda-url.us-east-1.on.aws maps to your function. The response is 403.

This is the same mechanism as API Gateway custom domains (see the post on API Gateway custom domains) — the underlying infrastructure routes by hostname.

CloudFront as the Host header rewriter

ConceptAWS Lambda / CloudFront

CloudFront solves this by acting as a proxy that sends the correct Host header to the Lambda Function URL origin. Client connects to CloudFront using your custom domain. CloudFront terminates the connection, then connects to the Lambda Function URL with the correct Host header. Your ACM certificate on CloudFront handles TLS for your custom domain.

Prerequisites

  • Lambda Function URLs
  • CloudFront origins
  • ACM certificates

Key Points

  • CloudFront origin_protocol_policy must be https-only for Lambda Function URLs.
  • ACM certificate must be in us-east-1 for CloudFront (edge-optimized).
  • Lambda Function URL authorization_type determines who can invoke: NONE (public) or AWS_IAM.
  • CloudFront cannot sign requests for Lambda IAM auth — use NONE and control access via other means if using CloudFront.

Full setup: Lambda Function URL + CloudFront + Route 53

# 1. Lambda function and Function URL
resource "aws_lambda_function" "api" {
  function_name = "api"
  handler       = "lambda_function.handler"
  runtime       = "python3.12"
  role          = aws_iam_role.lambda_exec.arn
  filename      = "lambda.zip"
}

resource "aws_lambda_function_url" "api" {
  function_name      = aws_lambda_function.api.function_name
  authorization_type = "NONE"  # CloudFront handles access control
}

# 2. ACM certificate (must be in us-east-1 for CloudFront)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

resource "aws_acm_certificate" "api" {
  provider          = aws.us_east_1
  domain_name       = "api.example.com"
  validation_method = "DNS"
}

# 3. CloudFront distribution
resource "aws_cloudfront_distribution" "api" {
  origin {
    # Lambda Function URL domain (strip https:// prefix)
    domain_name = replace(aws_lambda_function_url.api.function_url, "https://", "")
    origin_id   = "lambda-function-url"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = "lambda-function-url"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]

    cache_policy_id            = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"  # CachingDisabled
    origin_request_policy_id   = "b689b0a8-53d0-40ab-baf2-68738e2966ac"  # AllViewerExceptHostHeader

    min_ttl     = 0
    default_ttl = 0
    max_ttl     = 0
  }

  aliases = ["api.example.com"]

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.api.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  enabled = true
  comment = "Lambda Function URL proxy"
}

# 4. Route 53 alias record
resource "aws_route53_record" "api" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "api.example.com"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.api.domain_name
    zone_id                = aws_cloudfront_distribution.api.hosted_zone_id
    evaluate_target_health = false
  }
}

The critical CloudFront setting is origin_request_policy_id = "AllViewerExceptHostHeader". This managed policy forwards all viewer request headers except the Host header. The Host header sent to the Lambda origin is the Lambda Function URL's domain — not api.example.com. Without this, Lambda receives Host: api.example.com and returns 403.

📝Lambda Function URL vs API Gateway: when to use each

Lambda Function URLs provide a simpler, lower-cost way to expose Lambda over HTTP compared to API Gateway.

Use Lambda Function URLs when:

  • Simple HTTP endpoint for one function
  • No need for request validation, API keys, or usage plans
  • Response streaming (Function URLs support streaming; standard API Gateway doesn't)
  • Minimizing per-request cost (no API Gateway per-request charge)

Use API Gateway when:

  • Multiple Lambda functions under one domain with routing
  • Request/response transformation (mapping templates)
  • API keys and usage-based throttling per client
  • Cognito or custom Lambda authorizer authentication
  • WebSocket support
  • Built-in request validation against a schema
  • Developer portal / API documentation

Cost comparison (us-east-1, 1M requests/month):

  • Lambda Function URL: $0 (no additional charge beyond Lambda invocation cost)
  • API Gateway HTTP API: ~$1.00
  • API Gateway REST API: ~$3.50

For internal microservice communication, Function URLs via VPC origins provide lower latency than API Gateway VPC Link.

You set up CloudFront in front of a Lambda Function URL with authorization_type=AWS_IAM. Requests through CloudFront get 403 Forbidden. Direct requests to the Function URL with proper SigV4 signing work correctly. What is happening?

medium

The CloudFront distribution uses origin_request_policy AllViewerExceptHostHeader. The Lambda function URL has authorization_type=AWS_IAM. No Lambda@Edge or CloudFront Functions are configured.

  • ACloudFront strips the Authorization header before forwarding to Lambda
    Incorrect.AllViewerExceptHostHeader forwards the Authorization header. The issue isn't header stripping.
  • BCloudFront cannot sign requests with AWS SigV4 for Lambda IAM auth — requests forwarded by CloudFront are unsigned and rejected by Lambda's IAM authorization
    Correct!Lambda Function URL with authorization_type=AWS_IAM requires requests to be signed with AWS SigV4 credentials. CloudFront forwards the viewer's request without adding AWS signature headers — it doesn't have a mechanism to sign requests with IAM credentials on behalf of the viewer. The viewer's Authorization header (if present) is forwarded as-is, but the Lambda IAM auth checks if the request is properly signed by an IAM principal. Fix: use authorization_type=NONE and implement authorization in the Lambda function itself (validate a JWT, API key, or custom token from the request), or use a Lambda@Edge function to sign the origin request with IAM credentials.
  • CThe ACM certificate is causing SigV4 signature validation to fail
    Incorrect.TLS certificates and SigV4 request signing are independent mechanisms. The certificate handles connection security; SigV4 handles API authorization.
  • DCloudFront needs the lambda:InvokeFunctionUrl permission
    Incorrect.CloudFront itself doesn't need IAM permission to forward HTTP requests to a Function URL. IAM authorization for Function URLs is evaluated at the Lambda side based on the request's signing.

Hint:CloudFront is a proxy — it forwards requests but doesn't add AWS credentials to them. What does Lambda IAM auth require?