AWS CloudFront Functions
These things are wicked fast
CloudFront Functions
One Problem - Limit Page Hits to Match My Host
Let’s say you have a site, say, One Cup Coding.
This site is hosted out on CloudFront, pretty typical deployment, especially for placeholder/CTA sites. No worries, right?
Well, your .com site actually has a second domain when hosted via CloudFront, at https://xxxxxxxxxx.cloudfront.net. Ideally, you’d not want [bots] visitors to be able to stripe along *.cloudfront.net and resolve your site.
How can we fix this? Until recently, the answer was pretty obvious, and it was to write a Lambda@Edge function and deploy it to the edge. But now, we have access to a vastly faster, albeit limited, solution, CloudFront Functions. These things are wicked fast. First, though, some caveats:
CloudFront Function Limitations
- AWS CloudFront Functions use a modified ECMAScript 5.1 compatible runtime and NOT NodeJS.
- Use
var, notconstorlet. - Use string concatenation and not template literals.
- Memory is constrained to 2MB
A Fast Solution
Consider the following CloudFront function. It will:
- Return 403s for non-domain requests.
- Convert requests to
onecupcoding.comtowww.onecupcoding.com - Pass-through requests to
www.onecupcoding.com
'use strict';
/**
* @description: Handler entry point.
* @date 2022-05-21
* @param {object} event: A CloudFront Function event (expecting a Viewer Request event)
* @return {object}: A CloudFront response or request object (depends whether conditions allow pass through or 301 redirect)
*/
function handler(event) {
var request = event.request;
var queryString = queryStringObjectToString(request.querystring); // Note that this code does not appear below; it's found-code.
var uri = request.uri;
// Copy request.headers since it is read-only and we want to return a modified set of headers
var headers = JSON.parse(JSON.stringify(request.headers));
if (!headers.host) { headers.host = { value: "" }; }
var host = headers.host.value;
// Check to see if we have a properly formed or naked domain request
if (host === "www.onecupcoding.com") {
// Pass on the request as is
return request;
} else {
if (host === "onecupcoding.com") {
// Prefix the host with the missing www.
host = "www." + host;
// Construct the url with assumed/forced https://www. prefix plus, path and any query string parameters
var url = "https://" + host + uri + queryString;
// Append the new 301 location header
headers.location = { value: url };
// Remove the original host header to avoid a circular reference of never ending redirects
delete headers.host;
// Return a CloudFront specific response object that includes original path, query string, (most) headers and cookies
return {
statusCode: 301,
statusDescription: "Moved Permanently",
headers,
cookies: request.cookies,
};
}
delete headers.host;
return {
statusCode: 403,
statusDescription: "Access Denied",
headers,
cookies: request.cookies,
};
}
}
Another Problem - Improving Security Posture
Let’s say you have the same site and want to improve its security posture through returning all the security headers that the cool kids use. Again, you would traditionally either configure your site/Lambda/Lambda@Edge call to return these, but now it’s just a CloudFront Function away. Viz:
'use strict';
function handler(event) {
var response = event.response;
var headers = JSON.parse(JSON.stringify(response.headers));
headers.server = { value: "" };
headers['Content-Security-Policy'.toLowerCase()] = { value: "script-src 'self'" };
headers['Permissions-Policy'.toLowerCase()] = { value: "geolocation=(self 'https://www.onecupcoding.com'), microphone=()" };
var response = {
statusCode: response.statusCode,
statusDescription: response.statusDescription,
headers: headers
};
return response;
}
Caveats when messing with security headers
I was able to get away, for now, with locking the security headers down pretty tightly; this is, after all, a CTA-style page we’re looking at.
However, you may need to call a back-end service or reCAPTCHA or similar, so you may want to focus on the CSP header you return.
The inclusion of these headers, and the removal of the server header, took this site from a D rating to an A+ rating, and another site (which does invoke a back-end service through a form protected by reCAPTCHA) from an F to an A rating.
Useful!
Okay, I’m sold. How do I deploy these, exactly?
- Write the function.
- Determine where it should attach to the CloudFront call. For the above two functions, they are associated with the cache as
viewer-requestandviewer-responsefunctions, respectively. - Fold these into your IaC methodology of choice. I’m using
terraformbelow:
resource "aws_cloudfront_distribution" "onecupcoding_com" {
provider = aws.us-east-1
aliases = [
"onecupcoding.com",
"www.onecupcoding.com",
]
comment = "onecupcoding.com - CloudFront Distribution"
default_root_object = "index.html"
enabled = true
http_version = "http2"
# ...
default_cache_behavior {
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.deny_cloudfront_host.arn
}
function_association {
event_type = "viewer-response"
function_arn = aws_cloudfront_function.rewrite_outbound_headers.arn
}
# ...
}
# ...
}