A while ago I came across this post from Amazon that describes how to stream video securely using CloudFront signed URLs. However, I couldn’t find any infrastructure-as-code to implement the solution outlined in the blog post so I decided to implement it myself. The repository is here.
While writing the Terraform code, I discovered there were some gaps in the article. For example, no mention about having to set CORS headers. Also, because the article was published several years ago, some of the Python code use deprecated libraries, which I had to update. In this post, I’ll attempt to fill in the gaps. I recommend you take a look at the diagram in their post that shows how the solution works because I’m not going to duplicate that here.
You’ll need to create a keypair before you can deploy the solution. For context, the signer uses the private key to sign the URL and CloudFront uses the public key to validate the signature. From the terminal, run the following commands:
$ openssl genrsa -out private_key.pem 2048
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
By default, Terraform will look in the current directory for the keys. If you save them somewhere else, you’ll need to set the corresponding variables when running Terraform.
Amazon has published a document here that has more information about how to create a keypair.
Signing the Playlist URL
The first issue I encountered when implementing the Lambda function was that the RSA library that it uses has been archived and is no longer supported. Not ideal. However, when I got around to deploying the code and testing it, it didn’t work! The version of openssl I used to generate the public and private keys generated the private key in a format that was different to the one the RSA library was expecting. Instead of trying to get it to work using a library that is no longer maintained, I decided to modify the code to use a more modern (and maintained) cryptography library but that meant I had to create a Lamba layer for it. I’ve included the zip file in the repository so you don’t need to do anything. However, if you are curious about how I created it, check out this page here.
As per the article, this step doesn’t do any authentication so if somebody were to obtain the URL for signing the playlist, they would be able to watch the video(s), which defeats the purpose of setting up a secure streaming solution in the first place! This is left as an exercise for the user.
I’m not sure I’d deploy this part of the solution. It seems to me that a better solution would be to use one of the AWS SDKs (or even the CLI) for signing the playlist URL on the client side because presumably the user will have already been authenticated at this point so why duplicate the authentication process in AWS? Also, unless you’re going to use the automatic secret rotation feature of secrets manager, which isn’t the case here, I’d probably store the private key in the parameter store instead because it’s cheaper.
Rewriting the Playlist
Once the playlist URL has been signed, the next step is to intercept any requests for the playlist and rewrite its contents so the segment URLs contain the necessary credentials so they can be downloaded from the S3 bucket. For context, URLs signed by CloudFront include 3 parameters: Key-Pair-Id, Policy, and Signature. CloudFront uses these to determine if the URL is valid or not. However, when it passes the request to the Lambda function, it strips these parameters from the URL so the function doesn’t see them. This is a problem because we need these parameters to sign the URLs of the segments. The solution Amazon came up with is to duplicate these parameters in the URL by appending a value to the name of the each parameter specified by the environment variable KEY_PREFIX. For example, if the value of KEY_PREFIX is “-PREFIX” like in the article, a parameter Policy-PREFIX will be appended to the URL with the value of the Policy parameter.
Unfortunately, the article doesn’t show how to do this; they leave it to the imagination of you, the user. I have included an example in PHP that shows how this process works. It should also be possible to achieve the same thing by using a Lambda@Edge function instead of doing it on the client. If that works, it would mean the Lambda function that signs the playlist URL (see above) could do a redirect instead of returning a 200 status code and including the signed URL in the body of the response, which would be a more natural thing to do.
Deploying the Solution
Clone the repository. Next, run the following commands to deploy the solution to your AWS account:
$ terraform init
$ terraform plan -var="bucket_name=<bucket name>" -out=tfplan
$ terraform apply tfplan
Once the deployment has finished, you’ll need to upload your videos to the S3 bucket in the same directory structure in the article, e.g. <bucket name>/movies/movie_1/index.m3u8
The URL for obtaining the signed playlist URL will be: https://<cloudfront domain>/content/media/signurl?movie=1. You can get the domain name of the CloudFront distribution by running: terraform output cloudfront_domain_name. In the PHP file, set the $initialUrl variable (line 6) to this URL.
If you have PHP installed locally, you can start the built-in web server by running the following command: php -S localhost:8000. Open http://localhost:8000/example.php in a browser. If everything is working correctly, you should be able to play the video.
If you have any questions or comments, feel free to send me an email. I’ve had to disable comments for now because of bot spam, despite the fact that comments are moderated so I’m the only that sees them.