Background Link to heading
AWS introduced Function URLs for Lambda functions to enable public invocation access over the World Wide Web. Function URLs support IPv4 and IPv6, Cross Origin Resource Sharing (CORS), and IAM for access control. The format of these URLs is https://<url-id>.lambda-url.<region>.on.aws
.
You may want to use a custom domain or share an existing domain with a unique path pattern. You may also want to protect function invocation by implementing caching, Origin Shield, or Web Application Firewall (WAF). CloudFront provides these features and can use a Lambda Function URL in an origin declaration.
A major caveat with this approach is function authentication. It appears that the AUTH_TYPE
for the function URL must be set to NONE
, else CloudFront will return 403 Forbidden
. But then the function is publicly accessible for invocation outside of CloudFront by bots and bad actors, increasing error rates and costs.
One approach to this problem is to set a custom HTTP header in CloudFront, coupled with logic in the lambda function to filter out requests that don’t contain the header and expected value. This approach relies on obscurity, and though it saves compute resources if the function fails fast, it does not prevent function invocation.
Another approach is to enable the AWS_IAM
AUTH_TYPE
in the function URL configuration, then create a Lambda@Edge function bound to the Origin Request event to sign the requests manually.
Today I found an undocumented, native solution to this problem. CloudFront uses Origin Access Control (OAC) policies for restricting access to AWS origins like S3. It does this by signing its requests to the origin, just as the previously mentioned Lambda@Edge function does. For this to work, the OAC policy requires an understanding of the origin type.
The CloudFront Developer Guide, AWS CLI, AWS API, and Terraform AWS Provider documentation only list s3
and mediastore
as possible origin types. The AWS Console also does not include controls for adding an OAC to an origin with a custom domain (nor do Lambda functions or function URLs appear in the origin domain dropdown list). However, the CloudFormation documentation shows valid values for OriginAccessControlOriginType
as ^(s3|mediastore|lambda|mediapackagev2)$
. Why not probe further?
Solution Link to heading
I manually created an Origin Access Control configuration for lambda request signing using the AWS CLI, which produced no errors. The console then correctly showed the OAC with origin type “Lambda function URL”. Of course, editing the OAC or creating a new one in the console does not show Lambda as a possible origin type.
Further, I was able to successfully associate the OAC to the CloudFront distribution origin configuration both via the CLI and terraform without any errors. Finally, I tested the configuration, ensuring that requests were actually being signed by CloudFront and correctly verified by Lambda. It works flawlessly.
My steps for implementing the native solution are below, both for the CLI and Terraform.
Using the CLI Link to heading
Create a custom Origin Access Control configuration using the AWS CLI. The important part here is using the undocumented property OriginAccessControlOriginType=lambda
.
$ aws cloudfront create-origin-access-control \
--origin-access-control-config "Name=oac,SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=lambda"
Note the OAC ID.
Download your CloudFront distribution configuration (replace the id
value with the ID of your distribution).
$ aws cloudfront get-distribution-config --id XXXXXXXXXXXXX --output yaml > dist-config.yaml
You’ll need to edit dist-config.yaml
to make a few changes:
- Replace the key
ETag
withIfMatch
, keeping the value the same - Under the origin item for your lambda function URL, remove
OriginAccessIdentity
if it exists - Under the origin item for your lambda function URL, set
OriginAccessControlId
to the OAC ID from the previous step
Then update the distribution configuration.
$ aws cloudfront update-distribution --id XXXXXXXXXXXXX --cli-input-yaml file://dist-config.yaml
You’ll need a lambda permission for CloudFront to invoke the function when IAM access control is enabled. Note that the action is lambda:InvokeFunctionUrl
, not lambda:InvokeFunction
.
$ aws lambda add-permission \
--function-name "your function name" \
--statement-id "cloudfront-access" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn of your cloudfront distribution"
Then you can enable AWS_IAM
authentication in your lambda function.
$ aws lambda update-function-url-config --function-name "your function name" --auth-type AWS_IAM
Using Terraform Link to heading
The Terraform AWS provider will cause terraform apply
to fail if you use lambda
as the value for the origin_access_control_origin_type
argument in the aws_cloudfront_origin_access_control
resource. I opened an issue to add this undocumented value to the resource schema. In the meantime, you can create the OAC manually via the CLI and then hard code the OAC ID in your CloudFront distribution configuration.
$ aws cloudfront create-origin-access-control \
--origin-access-control-config "Name=oac,SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=lambda"
# Note the OAC ID
#
# Lambda
#
resource "aws_lambda_function" "fn" {
# ...
}
resource "aws_lambda_function_url" "url" {
function_name = aws_lambda_function.fn.function_name
authorization_type = "AWS_IAM"
}
resource "aws_lambda_permission" "from_cloudfront" {
function_name = aws_lambda_function.fn.function_name
principal = "cloudfront.amazonaws.com"
action = "lambda:InvokeFunctionUrl"
source_arn = aws_cloudfront_distribution.dist.arn
}
#
# CloudFront
#
data "aws_cloudfront_cache_policy" "disabled" {
name = "Managed-CachingDisabled"
}
# TODO: Wait for `lambda` origin type support
# https://github.com/hashicorp/terraform-provider-aws/issues/36660
# resource "aws_cloudfront_origin_access_control" "oac_lambda" {
# name = "oac-lambda"
# description = "OAC Policy for lambda function"
# origin_access_control_origin_type = "lambda"
# signing_behavior = "always"
# signing_protocol = "sigv4"
# }
resource "aws_cloudfront_distribution" "dist" {
# ...
origin {
# ... for the default origin
}
origin {
origin_id = "lambda"
# origin_access_control_id = aws_cloudfront_origin_access_control.oac_lambda.id
origin_access_control_id = "XXXXXXXXXXXXX" # TODO: see above. hard code for now
# Named capture group for extracting domain from the function URL
domain_name = regex("(?:(?P<pr>[^:/?#]+):)?(?://(?P<d>[^/?#]*))?(?P<p>[^?#]*)(?:\\?(?P<q>[^#]*))?(?:#(?P<f>.*))?", aws_lambda_function.fn.function_url).d
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only" # required for lambda origin
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
# ...
}
# Lambda path
ordered_cache_behavior {
path_pattern = "/api"
allowed_methods = ["GET", "HEAD", "POST", "DELETE", "OPTIONS", "PUT", "PATCH"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "lambda"
cache_policy_id = data.aws_cloudfront_cache_policy.disabled.id
compress = true
viewer_protocol_policy = "https-only"
min_ttl = 0
}
}
Troubleshooting Link to heading
An IAM authentication failure will result in a 403 Forbidden
error to the client with response header x-amzn-ErrorType=AccessDeniedException
. You should not get this error when accessing the function through CloudFront, but you should get this error when accessing the function URL directly. If you don’t, ensure the lambda function URL AUTH_TYPE
is set to AWS_IAM
.
To troubleshoot problems with signing, change the function URL AUTH_TYPE
back to NONE
and log the request headers to the console in your lambda script. A signed request from CloudFront will include the Authorization
, X-Amz-Security-Token
, and X-Amz-Date
headers. If you’re getting a forbidden error but your requests include these headers, make sure you specified a lambda permission granting your CloudFront distribution access to lambda:InvokeFunctionUrl
(not lambda:InvokeFunction
) on your lambda function resource (not the function URL resource).
Conclusion Link to heading
CloudFront supports native request signing to Lambda Function URL resources. I don’t know why this feature isn’t documented. When implemented, access to Function URLs can be restricted to CloudFront requests, enforcing security and caching policy while avoiding malicious invocations directly to the function URL.