Protect HLS streams on AWS using CloudFront and Lambda function
Tags: aws, streaming, protect, hls
Published: 2021-12-09 21:45:28

It is a common requirement to be able to ensure that only entitled viewers can access the HLS streams. The full-fledged option is to [protect the streams with DRM](https://eyevinntechnology.medium.com/securing-ott-content-drm-1af2c08fdd31) which also makes it harder to record and capture the video and audio from the playing device. Another option is to have the CDN to restrict access to the content. One mechanism that many CDN providers offer is to require an URL-signature to be able to fetch the specific content. In this blog post we will walk through how to achieve this on Amazon Web Services.

## Restricting access with signed URLs

We will assume that you have [a setup where the users must go via a CloudFront CDN](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html) to access the content and that it is not possible to access the content directly on the origin server (for example S3 or a private HTTP server). In this example we will use Amazon S3 as an origin server. To ensure that access needs to go through the CloudFront CDN we have:

- Associated an *origin access identity* user with the CloudFront distribution.
- Given the *origin access identity* user permission to read files in the S3 bucket.
- Removed permissions for anyone else to read the files in the S3 bucket.

## Specify signers

We need to specify who can use signed URLs and will choose the recommended option to use a *trusted key group* as the signing mechanism by creating a CloudFront key group. A signer that will create a CloudFront signed URL will have a public-private key pair. The signer uses the private key to sign the URL and CloudFront uses the corresponding public key to verify the signature. 

First we will create the key pair:

```
openssl genrsa -out private_key.pem 2048
```

Then we will extract the public key:

```
openssl rsa -pubout -in private_key.pem -out public_key.pem
```

We then upload the public key (`public_key.pem`) to CloudFront. Once uploaded we will create a key group where we will add the ID of the public key we just uploaded.

## Add signer to the CloudFront distribution

Go to the CloudFront distribution and choose the Behaviors tab. Choose Edit and then choose Yes for **Restrict Viewer Access**. Choose **Trusted Key Groups** and then select the key group you created previously.

You can verify that this works by trying to access a file that you were able to access before restricting viewer access.

## Creating a signed URL

A signed URL is made of the following components:

- Base URL for the file. This is the CloudFront URL (including query parameters) that you would use to access the file if you were not using signed URLs.
- The date and time that you want the URL to stop allowing access to the file.
- A hashed, signed, and base64-encoded version of the JSON policy statement. The policy statement includes base URL (resource) and the expiry date.
```
{
  "Statement": [
    {
      "Resource": "base URL or stream name",
      "Condition": {
        "DateLessThan": {
          "AWS:EpochTime": ending date and time in Unix time format and UTC
        }
      }
    }
  ]
}
```
- The ID for a CloudFront public key, for example, `K2JCJMDEHXQW5F`. The public key ID tells CloudFront which public key to use to validate the signed URL. CloudFront compares the information in the signature with the information in the policy statement to verify that the URL has not been tampered with.

As the policy includes both resource and expiration time the signature is unique for each file we want to access. This is when protecting HLS becomes a little bit tricky.

## The tricky part when using this method to protect HLS

For those not familiar with the details of Apple HLS it is based around a couple of text files that contains references to other resources. The first text file that the video player wants to access is the manifest. The manifest is a playlist containing a list of media playlists, where each playlist corresponds to a specific variant of the stream. It can for example be a variant encoded in a different resolution. A media playlist then in turn contains the list of media segments that the video player "stitches together" on the client side.

Accessing the manifest is simple as we can just use the signed URL for that resource. However this text will not include any signed URLs for the media playlists. And the same goes for the media playlists, they will not contain any signed URLs for the media segments. Why don't we just store signed URLs in the manifest or media playlist file you might ask? The answer is that we want to have an expiration time that is short, otherwise an authenticated user can obtain the signed URL for the manifest and share it with others. This means that we need to intercept and modify the manifests and add signed URLs for the media playlists and media segments.

## Lambda function to intercept and insert signed URLs

To accomplish this we will develop a Lambda function that will intercept a manifest and media playlist request and update it with signed URLs. The process can be illustrated with the sequence diagram below.

![Sequence diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnyql7fzoiyvrhopta2a.png)

In this example we will use the simple authentication method of using the Basic auth scheme. A mechanism that most modern browsers support. In a real scenario you would typically replace this with a session based bearer token that an authenticated client has received when being entitled to access the asset.

1. A video player wants to access the HLS manifest that contains the list of media playlists.
2. The Lambda function will return `401 Unauthorized` and (in this case) provide the client the instructions to use Basic auth.
3. The video player tries again but now provides the HTTP header `Authorization` which content has been provided by the browser.
4. The Lambda function uses the private key to create a signed URL for the manifest, and fetches the content from CloudFront.
5. The Lambda function then uses the same mechanism to generate the signed URLs for the media playlists. Important to note here is that it needs to use the CloudFront URL as the base URL for the signature, as it is this signature that will be used to fetch the media playlist from CloudFront.
6. When the video player wants to access the media segments it will fetch the media playlist that now includes the signature generated in (5).
7. The Lambda function will fetch the contents for the media playlist from CloudFront using the signature it has provided. So in this step no new signed URLs are created. This is to ensure that the client actually has received a valid signature in previous steps.
8. The Lambda function then signs the URLs for each media segment in the playlist. It also adds the CloudFront host as the absolut URL in the list. As no further requests should go through the Lambda function.
9. The video player access the media segments directly from CloudFront using the signed URLs it has been provided in (8).

```javascript
export const handler: ALBHandler = async (event: ALBEvent): Promise<ALBResult> => {
  console.log(event);
  // This is needed because Internet is a bit broken...
  const searchParams = new URLSearchParams(Object.keys(event.queryStringParameters).map(k => `${k}=${event.queryStringParameters[k]}`).join("&"));
  for (let k of searchParams.keys()) {
    event.queryStringParameters[k] = searchParams.get(k);
  }
  console.log(event.queryStringParameters);
  let response;
  try {
    if (event.path.match(/\.m3u8$/) && Object.keys(event.queryStringParameters).length > 0 && event.httpMethod === "GET") {
      response = await handleMediaPlaylistRequest(event);
    } else if (event.path.match(/\.m3u8$/) && event.httpMethod === "GET") {
      response = await handleBasicAuthMultiVariantRequest(event);
    } else if (event.httpMethod === "OPTIONS") {
      response = await handleOptionsRequest(event);
    } else {
      response = generateErrorResponse({ code: 404, message: "Resource not found" });
    }
  } catch (error) {
    console.error(error);
    response = generateErrorResponse({ code: 500, message: error.message ? error.message : error });
  }
  return response;
};
```

An example of this Lambda function is available on our [GitHub](https://github.com/Eyevinn/lambda-protect-hls).

You can also try it out online by pointing your HLS video player to: `https://hls-signed.lambda.eyevinn.technology/DEC6_TEST_002/master.m3u8` (credentials are `eyevinnpoc` as username and password).

## About Eyevinn Technology
Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor.

At Eyevinn, every software developer consultant has a dedicated budget reserved for open source development and contribution to the open source community. This give us room for innovation, team building and personal competence development. And also gives us as a company a way to contribute back to the open source community.

Want to know more about Eyevinn and how it is to work here. Contact us at work@eyevinn.se!