Restricting AWS Lambda Function URLs to CloudFront
Tags: aws, serverless, tutorial, cloud
Published: 2023-03-07 19:44:15

AWS Lambda Function URLs are a great thing that fits seamlessly into AWS’s serverless vision. Combined with S3 static hosting and CloudFront, it is the ideal platform for high performance website hosting without the hassle of managing a complex underline infrastructure.

## The basics: S3 static website hosting

Hosting your static website has never been easier. With Amazon S3 static hosting, you can serve your static pages by simply uploading it to an S3 bucket and enabling public access (be sure to name your bucket as your domain name). You can find a lot of articles on the web that explain how to set up S3 static hosting, which is why I am not going to go into any further details here.

But there are limitations: S3 static hosting doesn’t support HTTPS, the de-facto-minimum for website hosting. To use HTTPS, you need to set up Amazon CloudFront. This comes with a lot of extra features like GeoIP restrictions, caching and a free SSL certificate. Not to mention, you can finally disable your S3 public access (which could be a security risk) and give limited access to CloudFront only (with a bucket policy).

> Pro tip: Give CloudFront ListBucket permissions in your S3 bucket policy, otherwise the client will not receive HTTP status codes, including a 404 when trying to access non-existent content:

```
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::roadtoaws.com",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::111111111111:distribution/AAAAAAAAAAAAA"
                }
            }
        },
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::roadtoaws.com/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::111111111111:distribution/AAAAAAAAAAAAA"
                }
            }
        }
    ]
}
```

Because of the caching involved with CloudFront, this is not ideal for development. You either have to test your code locally or without HTTPS enabled.  This is the main reason why I would still like to see HTTPS support in S3 in the future. 🔮

## Make it dynamic

Static websites are a thing of the past. You will most likely need some kind of dynamic content. While there are a lot of services that provide functionality, like E-mail sending, Comments, that you could include in your static code to make it dynamic, you’d most likely have to write your own code. This is where Lambda Function URLs come in handy. With a simple Lambda function, you can execute code or use other AWS resources that you can invoke with a simple HTTP request in your browser. But how do you restrict it to a specific IP, domain, or CloudFront? 🤔

![Function URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjvl78g1i9bx8dtui80y.png)

AWS recommends authenticating through IAM, and while this is really a secure way, it makes development challenging.  The first thing you see is CORS where you can set your origin to a domain. Unfortunately, this didn’t work for me the way I wanted it to. This doesn’t restrict your Lambda from being called from any IP. You can also set an X-Custom header here, but that doesn’t really limit external access.

Then you look for matching IAM permissions that you can attach to Lambda functions. In the available Policies you can find InvokeFunctionUrl where you can add an IP address to limit the invocation to a specific IP. This sounds great! You create a policy and attach it to your Lambda Role. Unfortunately, this does not restrict your Lambda access either.

So what was my solution? 🙋🙋🙋

## 1. Restrict in code

The first obvious solution is to check the source IP with your Lambda function. Here is a sample code in Node.js (you can find a similar code for other languages online):

```
const ipAddress = event.identity.sourceIP;

if (ipAddress === '52.84.106.111') {
  const error = {
      statusCode: 403,
      body: JSON.stringify('Access denied'),
  };
  
  return error;
} else {
  const hello = {
      statusCode: 200,
      body: JSON.stringify('Hello World!'),
  };
  
  return hello;
}
```

While this obviously works, you’re adding extra code to a Lambda function that’s primary role is to do something else. Not to mention that this will increase the runtime and the resources used by Lambda. Most importantly, how can you be sure that the IP you get in the sourceIP variable is really the IP the client comes from.

My biggest concern with this solution was that I not only wanted to restrict my functions to one specific IP but to the whole CloudFront distribution – so that I can be sure that it is called from one of my static pages –. With this method, it would be a hassle to maintain an up-to-date list of all CloudFront servers. 📝📝

## 2. reCAPTCHA

Yes, you heard it right, Google reCAPTCHA. This may sound strange at first, but this is the solution I have implemented in my work and provides the solutions to the above challenges.

Embeding the reCAPTCHA code in your static web pages is a good idea. In fact, Google recommends that you include the code in all of your pages – not just the ones that you need it, such as form validations – because that way the algorithm can more effectively detect fraudulent use. Within the lambda function, I can now validate whether or not the user really invoked my Lambda function URL from my static web page. Here is the code I use to verify the reCAPTCHA request:

```
const gRecaptchaResponse = event.queryStringParameters["g-recaptcha-response"];
    
    var verificationUrl = "https://www.google.com/recaptcha/api/siteverify?secret=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&response=" + gRecaptchaResponse;
    const recaptchaResult = await getRequest(verificationUrl);
    
    if (false == recaptchaResult.success || 0.5 > recaptchaResult.score) {
      return error;
    }
```

## In conclusion

S3 static website hosting is the easiest way to start with your serverless journey. While there are obstacles ahead you can always find a serverless solution. 🏆