Lambda Function URLs with IAM Authorization and CloudFront Custom Domains

// 3 comments

In this short article, I'd like to share some insights I gained from a recent issue involving the combination of Lambda Function URLs, IAM authorization, and custom CloudFront domains. I hope it will help someone out there who may be struggling with this exact problem right now. So, without further ado, let's get started.


Lambda Function URLs are a powerful feature. They notably extend the HTTP timeout to up to 15 minutes, a significant increase from the 30 seconds provided by API Gateway, and also support response streaming. Lambda Function URLs typically follow this format:

https://<url-id>.lambda-url.<region>.on.aws

To secure your Lambda function against unauthorized access, you can attach an IAM authorizer to its URL. This process involves signing your HTTP request with IAM credentials according to the AWS Signature V4 specification. A convenient way to handle this is by using libraries like aws-sigv4-fetch or similar tools, which automate the signature calculation and add the Authorization header to your HTTP request.

Attempting to invoke your Lambda Function URL without a valid Authorization header will result in a 403 Forbidden response.

Invoke
Invoke Lambda Function URL without valid credentials

However, signing the request with appropriate credentials, as shown here using Thunder Client, should yield a successful response.

Invoke
Invoke Lambda Function URL with valid credentials

Now, you might be tempted to assign a custom domain to your Lambda function because you want to call the function from a different service and not have to worry about cross-stack imports. For example, I like to add the deployment phase (development, staging, production) as a subdomain to my custom domains:

https://<service>-<stage>.example.com

Unlike API Gateway, Lambda Functions do not natively support custom domains. However, they can be configured manually using CloudFront and Route53, which both services use under the hood. Setting up a CloudFront distribution to use Lambda Function URLs as a custom origin forwards all requests to your Lambda Function.

CloudFront
CloudFront distribution with Lambda Function URL as origin

So far, so good. Now let's repeat the previous HTTP request, but with the custom domain. By the way, I have not assigned an alternative domain name and simply used the CloudFront domain here, but the result will be the same:

Invoke
Invoke CloudFront distribution URL with valid credentials

Status 403 Forbidden...Okay, what happened here? I assume that everyone who has worked with Signature V4 has come across this error message at some point:

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.

To understand this message, let's start with a quick summary of what Signature V4 does. If you want to send an HTTP request to an AWS service, you need to send your identity and proof of authenticity with every HTTP request. According to the Signature V4 spec, you must write down each HTTP request in a specific way. This is your canonical request and it is very important that two identical HTTP requests result in the same canonical request. The canonical request is concatenated with other metadata to form a signable string, which is then hashed (SHA256) using your AWS credentials. This hash is added to your HTTP request as an Authorization header.

AWS
AWS Signature V4 signing process

AWS replicates this process upon receiving your request and calculates the signature for it. Since AWS has access to the access key and the secret key that you used for the calculation, AWS should be able to create the same signature. If this is not the case, access will be denied with the error message mentioned above.

So, what went wrong along the way? Let's take a look at the behavior of CloudFront, which forwards the request to the Lambda function. The origin request policy controls how CloudFront forwards the request to Lambda:

CloudFront
CloudFront origin policies

The AllViewerExceptHostHeader origin request policy is recommended for Lambda Functions URLs. It is a managed policy, which means we do not have to create it ourselves. This is how it is defined:

CloudFront
CloudFront managed origin request policy

As the name implies, this policy forwards all HTTP headers of the client request, except the Host header. But why is this important? When you call your Lambda function via its function URL, you are actually calling the Lambda service in a specific region. This service receives your request and identifies the Lambda function to be called using the unique ID from the Lambda function URL. It then calls your Lambda function with the HTTP request as the payload. For this to work, CloudFront must change the Host header from the CloudFront domain <distribution-id>.cloudfront.net of the client request to the domain of your Lambda Function URL <url-id>.lambda-url.<region>.on.aws.

Next comes the IAM authorizer, which is assigned to your Lambda function to protect it from unknown invocations. The IAM authorizer receives the HTTP request from CloudFront and checks its validity. It repeats the process to calculate the signature for this HTTP request according to the Signature V4 specification. However, the request from CloudFront has been slightly modified by the Host header. Yet this small change results in a completely different signature. The signature from the client does not match the calculated signature and therefore the request is denied with status 403 Forbidden and the error message shown above.


So what is next? There is a possible workaround for this issue by using the Lambda Function URL as the Host to calculate the signature, but then sending the request to CloudFront. This means that you calculate the signature that the IAM authorizer will calculate after CloudFront has changed the host to the Lambda Function URL. I don't really recommend this approach, but I wanted to mention it anyway. Personally, I would like to see a change in the combination of CloudFront and Lambda Function URLs. For example, CloudFront could leave the Host header unchanged and instead add the Lambda Function URL to a custom header like X-Forwared-Host. These headers are not included in the Signature V4 singing process and therefore do not change the calculated signature. Lambda could still check the Host header, but would fall back to the X-Forwared-Host header as an alternative.