Networking & Content Delivery

Serving Private Content Using Amazon CloudFront & AWS Lambda@Edge

In this blog post, I will show you how to configure your Amazon CloudFront distribution using Lambda@Edge to serve private content from your own custom origin. To learn more about edge networking with AWS, click here.

Amazon CloudFront is a global content delivery network (CDN) service that securely accelerates the delivery of websites, applications, videos, large files, and APIs to your viewers around the world. In general, Amazon CloudFront can be configured in conjunction with different types of origins from which it will fetch your content. For example, you can configure your origin in AWS (e.g. Amazon S3, Amazon EC2, Elastic Load Balancing, Amazon API Gateway…) or on any public HTTP(S) endpoint. When you configure an AWS origin, you benefit from:

  • Cost optimization: Data transfer out costs from an AWS origin to CloudFront Edge Location is free of charge.
  • Improved performance: Connections to other parts of AWS are made over high-quality networks that are monitored by Amazon for both availability and low latency. This monitoring has the beneficial side effect of keeping error rates low and window sizes high.

Lambda@Edge makes CloudFront a programmable content delivery network by running AWS Lambda functions in CloudFront locations closer to your viewer to customize your content. Lambda@Edge lets you run Node.js code without provisioning or managing servers. Your code can be executed in response to CloudFront events such as requests for content by viewers or requests from CloudFront to origin servers.

Many companies that distribute content via the internet want to restrict access to documents, business data, media streams, or content that is intended for specific users such as those who have paid a fee. To securely serve this private content using CloudFront, you need to do the following:

  • Require that your users access your private content by using special CloudFront signed URLs or signed cookies.
  • Restrict access to your origin exclusively to CloudFront.

In this rest of this blog post, I will focus on the second point, how to restrict access to your origin using CloudFront and Lambda@Edge. Generally speaking, you can enforce access control to your origin using several techniques:

  • Configure Origin Access Identity to restrict access to content on Amazon S3.
  • Whitelist Amazon CloudFront IPs on your custom origin’s firewall. A custom origin is an HTTP(S) endpoint, for example, an HTTP server on an Amazon EC2 instance or an HTTP server that you manage privately. If you have configured origin in a VPC, you can automatically update your Security Groups by using AWS Lambda.
  • Configure CloudFront distribution to include a custom header carrying a shared secret whenever it forwards a request to your custom origin. You need to specify the header name and its value. For example, when using Amazon API Gateway as origin, you can configure x-api-key header with your API key value as custom header.

However, if your custom origin requires special access control logic, you can use Lambda@Edge as explained in the following three steps. In my example, I setup a custom origin based on a NGINX HTTP server with Secure Link Module. This module requires signed URLs to access private content on the server, otherwise the request is forbidden:

Step 1: Create Amazon CloudFront distribution

In the AWS Management console, create a new Web distribution:

Then configure your own custom origin domain name, select your accepted SSL protocols, configure the Origin Protocol Policy to HTTPS only, and set your timeouts for Origin Response and Origin Keep-alive. For the sake of simplicity, I left the rest options to their default configurations.

Step 2: Create Lambda@Edge function

In the AWS Management console, go to the AWS Lambda service console, and author a new Lambda function in us-east-1 region:

Next, you will need to create a credentials.json file in which you paste the secret key that you can create using the Secure Link Module on NGINX.

Finally, use the below Node.js code in the main index.js file:

'use strict';

const crypto = require('crypto');
const credentials = require('credentials.json');
const secret = credentials.secret;

exports.handler = (event, context, callback) => { 
    const request = event.Records[0].cf.request;
    
    const expires = generateExpires();
    const signature = "md5="+generateSecurePathHash(expires, request.origin.custom.path + request.uri)+"&expires="+expires;
    
    if (request.querystring) {
        request.querystring = request.querystring + "&" + signature;
    } else {
        request.querystring = signature;
    }
    
    callback(null, request);
}

function generateExpires() {
    // Set expiration time to 1 hour from now
    const date = new Date();
    date.setHours(date.getHours()+1);
    return Math.floor(date/1000);
}

function generateSecurePathHash(expires, URL) {
    // construct string to sign
    const unsignedString = expires + URL  + ' ' + secret; 
    
    // compute signature
    const binaryHash = crypto.createHash("md5").update(unsignedString).digest();
    const base64Value = new Buffer(binaryHash).toString('base64');
    return base64Value.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

In the above code, the Lambda@Edge function does the following:

  • Construct the string to be signed.
  • Compute MD5 signature, encode it in Base64 and replace some special characters.
  • Add expiration and signature to the origin request using query strings.
  • Forward request to the origin

Step 3: Associate Lambda@Edge function to your CloudFront distribution

In general, you can trigger Lambda@Edge functions at the following points:

  • Viewer Request: Executed on every request before CloudFront’s cache is checked
  • Viewer Response: Executed on all requests, after a response is received from the origin or cache
  • Origin Request: Executed on cache miss, before a request is forwarded to the origin
  • Origin Response: Executed on cache miss, after a response is received from the origin

In this use case, I will use Origin Request trigger to execute Lambda@Edge function whenever Amazon CloudFront needs to get the object from the origin upon a cache miss.

First, from the Lambda console, publish a new version of your function

Then enable it on CloudFront distribution with Origin Request trigger:

In the console, wait until your CloudFront distribution status shows ‘Deployed’ and then try and access the private content using your CloudFront domain:

Conclusion

In this blog post, we configured CloudFront using Lambda@Edge to sign requests to private content on your custom origin. This solution represents one simple example of a variety of possible use cases where you can take advantage of Lambda@Edge’s customization power.

To learn more about the services used in this example, please visit the Getting Started with Amazon CloudFront and Getting Started with AWS Lambda@Edge documentation for more information on how to get started with our services today.

Blog: Using AWS Client VPN to securely access AWS and on-premises resources
Learn about AWS VPN services
Watch re:Invent 2019: Connectivity to AWS and hybrid AWS network architectures