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.
However, signing the request with appropriate credentials, as shown here using Thunder Client, should yield a successful response.
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.
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:
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 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:
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:
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.
Top comments (3)
Amazing high quality article, as always. π
If you want a custom domain in front of an IAM auth furl, probably the best place to do your sigv4 signing is using a Lambda@Edge origin request function. Calculating the signature on the client based on the lambda url also works. It's unlikely the Lambda team will also support the X-Forwarded-Host header, without configuration knowing about the custom domain.
Is there something you are trying to gain with IAM auth? The timeout with CloudFront can be 60 seconds without a limit increase, so it's only 2X APIG.
Another option to secure your furl is to set your auth to NONE but check for a shared secret header value in your furl code and set that header on requests to the origin in CloudFront. It's less secure than IAM auth, and requests get into your lambda code before they are rejected, but depending on your use case it may work.
Signing the request on Lambda@Edge only ensures that the Lambda function is only callable from the CloudFront and not directly via its Function URL. But it doesn't address the authorization problem.
I actually hope they will, CloudFront already sets a few other
x-forwarded-*
headers. I don't know if the Lambda team will add support for it, but it would be an easy fix to get IAM auth to Lambda through CF working.I think double the timeout is already a good bargain :-)
Lambda Function URLs only support IAM auth, so there's no alternative to it. I'd like to avoid custom auth options, because, as you said, tehy are less secure and will invoke the Lambda on every (unauthorized) call.