Enhancing security for Lambda function URLs
Tags: aws, security, serverless
Published: 2024-05-02 09:09:31

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 <a href="https://aws.amazon.com/about-aws/whats-new/2024/04/amazon-cloudfront-oac-lambda-function-url-origins/" class="link" target="_blank" rel="noopener noreferrer">announced</a> the support for <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html" class="link" target="_blank" rel="noopener noreferrer">CloudFront</a> Origin Access Control<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" class="link" target="_blank" rel="noopener noreferrer"> with Lambda function URL</a> 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 <a href="https://dev.to/aws-builders/controlling-access-to-lambda-function-urls-idd" class="link" target="_blank" rel="noopener noreferrer">an article</a>. 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 <a href="https://dev.to/aws-builders/securing-s3-uploads-and-downloads-with-origin-access-control-3fl0" class="link" target="_blank" rel="noopener noreferrer">Origin Access Control</a>, 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**:

```json
{
  "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:

```sh
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 <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html" class="link" target="_blank" rel="noopener noreferrer">IAM</a> 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 <a href="https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html" class="link" target="_blank" rel="noopener noreferrer">DDoS protection</a> or <a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html" class="link" target="_blank" rel="noopener noreferrer">WAF</a>, 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 <a href="https://dev.to/aws-builders/protecting-apis-with-custom-headers-in-cloudfront-3m1k" class="link" target="_blank" rel="noopener noreferrer">custom header</a> 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 <a href="https://twitter.com/rooToTheZ/status/1788606025265975505?t=9KPIsbosyK2_60RKlVl5lg&s=19" class="link" target="_blank" rel="noopener noreferrer">X post</a>, 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:

```json
{
  "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:

```json
{
  "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

<a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html" class="link" target="_blank" rel="noopener noreferrer">Creating and managing Lambda function URLs</a> - How to create Lambda function URLs

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-creating-console.html" class="link" target="_blank" rel="noopener noreferrer">Creating a distribution</a> - How to create a CloudFront distribution

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#create-oac-overview-s3" class="link" target="_blank" rel="noopener noreferrer">Creating a new origin access control</a> - OAC for S3 (the process is the same for Lambda origins)