Enhancing security for Lambda 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)