Enhancing security for Lambda function URLs

AWS has introduced support for Origin Access Control on Lambda function URLs. This new feature ensures more secure and consistent content delivery for our function URLs.

1. The scenario

AWS has recently announced the support for CloudFront Origin Access Control with Lambda function URL origins.

I’ve experimented with this new feature and recapped my findings in this post.

2. Steps

Instead of digging into detailed instructions for creating each resource, I will outline the necessary steps here. You can find links to the relevant documentation pages at the end of this article.

2.1. Lambda function with URL

The first step involves setting up a Lambda function with an enabled function URL using the AWS_IAM auth type. I previously discussed the different function URL authorization types in an article. To summarize, the AWS_IAM auth type secures the endpoint by only accepting signed requests, while the AuthType: NONE configuration opens up the URL to the public.

2.2. CloudFront distribution

Next, we create a CloudFront distribution using the Lambda function URL as the origin. We copy the URL and specify it as the Origin in the distribution.

2.3. Origin Access Control (OAC)

The next step is to create an Origin Access Control in CloudFront and specify Lambda as the origin. We should then add the OAC to the distribution.

In a previous post on Origin Access Control, I discussed how OAC can limit direct access to an S3 bucket. We follow a similar principle here.

2.4. Add the permissions

Finally, to allow the CloudFront distribution to invoke the function URL, we need to add the following permission to the Lambda function’s resource-based policy:

{
  "Effect": "Allow",
  "Principal": {
    "Service": "cloudfront.amazonaws.com"
  },
  "Action": "lambda:InvokeFunctionUrl",
  "Resource": "arn:aws:lambda:eu-central-1:ACCOUNT_ID:function:FUNCTION_NAME",
  "Condition": {
    "ArnLike": {
      "AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
    }
  }
}

Since the AWS console doesn’t provide a JSON editor for creating or editing Lambda resource policies, we either use the UI or add the policy via the CLI. Here’s how we can add it using the CLI:

aws lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipal" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID" \
--function-name FUNCTION_NAME

3. Results

In summary, the setup involves a Lambda function URL protected by IAM and a Lambda resource policy allowing our CloudFront distribution to invoke this URL.

Let’s see the results of my experimentation.

3.1. Calling the function URL directly

First, I tested the function URL directly by making unsigned requests, which, as expected, were all declined. Then, when I signed the requests using my credentials, they were successfully processed.

This behaviour is expected since the AuthType: AWS_IAM configuration is designed to protect the endpoint. Only identities granted with the lambda:InvokeFunctionUrl permission can successfully call the URL. Thus far, the results confirm the expected security behaviour.

3.2. Calling the URL via CloudFront

Next, I tested invoking the function via the CloudFront domain, simulating an external user making unsigned requests. As expected, the GET request was successful, and I received a response from the Lambda function.

I anticipated this outcome. While the AWS_IAM auth type secures the function URL by only serving signed requests, the Lambda resource policy allows access to the function through the CloudFront distribution. Hence, invoking the function through the CloudFront domain name returned a valid response.

But when I attempted a POST request, it failed with the following error message:

The request signature we calculated does not match the \
 signature you provided. Check your AWS Secret Access Key \
 and signing method. Consult the service documentation for details.

I didn’t expect this result. When I reviewed the documentation, I found the following note:

If you use PUT or POST methods with your Lambda function URL, \
your user must provide a signed payload to CloudFront. Lambda \
doesn't support unsigned payloads.

The documentation indicates the payload itself requires signing with proper credentials for POST requests. External users would not be able to perform this step.

CloudFront enhances security with additional layers such as DDoS protection or WAF, and also provides content delivery acceleration. We can also assign a custom domain to the distribution, providing a more user-friendly name than the complex function URL domain.

However, when handling POST requests, the situation remains consistent whether the function URL is invoked directly or via CloudFront. All such requests must be signed so only authorized identities can execute POST operations.

So, while the new feature enables easier access for GET requests, POST requests still require signed payloads. It restricts using function URLs with CloudFront distributions to those who can sign the requests with the necessary credentials.

4. Considerations

Currently, the benefits of using CloudFront with Lambda function URLs are limited to GET requests if we aim to have the function URL protected. For POST, PATCH, and PUT methods, signing the requests with AWS credentials (access key, secret access key, and session token for roles) is required. For instance, we can’t have the CloudFront distribution’s domain as a webhook URL because these endpoints typically utilize POST methods.

A practical enhancement would be enabling CloudFront to automatically sign the request payloads and forward them to the function URL origin. This way, we could use CloudFront in more use cases, like webhook implementations.

Additionally, unlike S3, Lambda does not support locking function URL invocations to specific resources like a CloudFront distribution. We can’t add a Deny statement to the function’s resource policy!

One way to restrict the function’s business logic to invocations only from CloudFront is to add a custom header to origin requests and verify the header’s presence in the Lambda function. With this approach, the Lambda function executes each time a request is made, if only to check for the header, leading to an increased number of function invocations.

The new feature is an enhancement in making function URLs a more attractive option for certain use cases. Now, we can make GET requests to the function URL via CloudFront without needing additional header validation code in the function’s handler while maintaining the URL protection through IAM.

However, the current limitations around POST and PUT requests mean that we can use them with CloudFront and Origin Access Control (OAC) mostly in internal applications. In these scenarios, we can configure the resource that’s making the call to sign the request payloads using its AWS credentials.

UPDATE 2024-05-13

In a recent X post, fellow Community Builder David Behroozi highlighted that the architecture described above supports both POST and PUT requests. To make it work, we need to compute the SHA256 hash of the body and include the hash in the x-amz-content-sha256 header.

Consider this example, where the request body is:

{
  "test": "message"
}

For the above body, the SHA256 hash would be 7acb899e28935d98d2a01e27212b5f130ca860f5899c1caabf18ec0988e5df62. This hash should then be included in the request headers as shown below:

{
  "headers": {
    "x-amz-content-sha256": "7acb899e28935d98d2a01e27212b5f130ca860f5899c1caabf18ec0988e5df62"
  }
}

By following these steps, sending POST requests to the CloudFront endpoint should yield successful responses.

Thanks to David for this great insight!

5. Summary

The integration of CloudFront Origin Access Control with Lambda function URLs introduces an additional layer of security, enabling us to keep function URLs private.

With this new feature, external users can now initiate GET requests via the CloudFront domain. This release makes Lambda function URLs a more viable and secure option for various use cases.

6. Further reading

Creating and managing Lambda function URLs - How to create Lambda function URLs

Creating a distribution - How to create a CloudFront distribution

Creating a new origin access control - OAC for S3 (the process is the same for Lambda origins)