Let's Encrypt CloudFront Cert Renewal with Lambda

January 2, 2016
aws lambda cloudfront letsencrypt ssl

Update: AWS has since released Amazon Certificate Manager that allows for certificates to be provisioned for free on CloudFront with certificate auto rotation enabled. I’ll keep this post here for historical reasons.


In this tutorial, I will show you how to setup a regularly scheduled AWS Lambda function to renew a Let’s Encrypt certificate hosted on CloudFront.

Currently, this blog is statically generated with Hugo[https://gohugo.io/] and is being served out of an S3 bucket with CloudFront sitting in front for caching purposes. This post won’t cover setting up a static website, but if you’d like details on how to do this please see the documentation for hosting a static website on AWS.

It is possible to enable SSL with CloudFront for a website being served from S3. In order to get a free certificate, I’m using Let’s Encrypt, a new Certificate Authority aiming to make installation of SSL certificates free and automated. They offer a Python based CLI for automating creation and renewal of certificates. This CLI has support for plugins, and luckily a plugin, letsencrypt-s3front, has already been created for working with CloudFront. Currently Let’s Encrypt certificates expire every 90 days, so having an automated process around renewals is very useful.


A high level overview of the steps required to make this happen:

Launching an EC2 instance

This instance will be used to gather all dependencies needed for creating the Lambda function. Since Lambda functions are under the covers executing on Amazon Linux, it is important to use this as the build environment to ensure compatibility. I will assume you are capable of deploying an EC2 instance running Amazon Linux. In my case, I deployed a t2.micro instance running Amazon Linux AMI 2015.09.1. Once deployed, SSH into your instance.

ssh -i <key_name>.pem ec2-user@<public_ip>

Gathering all dependencies

Make sure all packages are up to date, and install required system packages needed for letsencrypt CLI. Dependencies are documented here.

sudo yum -y update
sudo yum -y install -y \
       gcc \
       dialog \
       augeas-libs \
       openssl-devel \
       libffi-devel \
       redhat-rpm-config \

Setup a virtualenv environment and use pip to install letsencrypt and letsencrypt-s3front plugins.

virtualenv env
source env/bin/activate
pip install letsencrypt letsencrypt-s3front

Create a directory for holding files called ~/lambda. Copy letsencrypt CLI to ~/lambda directory (make sure to add .py extension to target or directory and file names will clash in final Zip that will be created). Also, to workaround an issue with the zope module, which is missing an init.py file, create a directory with this missing file.

mkdir ~/lambda
cp ~/env/bin/letsencrypt ~/lambda/letsencrypt.py
mkdir ~/lambda/zope
touch ~/lambda/zope/__init__.py

Write Python code to execute CLI command

Create a new Python file named main.py in the ~/lambda directory. Fill in appropriate variables for your site. This Python function will just shell out and execute the letsencrypt CLI with all required parameters. It will then copy all files created by the CLI (including the private key for your cert) to an S3 bucket of your choosing (e.g. yourdomain.com-certs).

cat << EOF > ~/lambda/main.py
import subprocess
import os
import boto3
import logging

# Global variables
email = "youremail@yourdomain.com"
domain_name = "yourdomain.com"
s3_website_bucket = "yourdomain.com"
region = "us-west-2"
cloudfront_distribution_id = "E3QXXXXXXXXX"
destination_s3_cert_bucket = "yourdomain.com-certs"
temp_dir = "/tmp"

# Open S3 connection and setup logger
s3_client = boto3.client('s3')
logger = logging.getLogger()

# Handler that will be called by Lambda
def handler(event, context):

    # Command line to execute to create/renew certificate
    command = "python letsencrypt.py --agree-tos -a letsencrypt-s3front:auth --letsencrypt-s3front:auth-s3-bucket {} " \
              "--letsencrypt-s3front:auth-s3-region {} -i letsencrypt-s3front:installer " \
              "--letsencrypt-s3front:installer-cf-distribution-id {} -d {} --email {} --keep --config-dir {} " \
              "--work-dir {} --logs-dir {} --no-redirect --text".format(s3_website_bucket, region,
                                                                        cloudfront_distribution_id, domain_name, email,
                                                                        temp_dir, temp_dir, temp_dir)

    # Execute command line and get results
        output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True, universal_newlines=True)
    except subprocess.CalledProcessError as e:
        logger.error("Failed to create/renew certificate. Error code: {}. Error output: {}".format(e.returncode, e.output))

    # Copy off all resulting files to private S3 bucket
    for root, dirs, files in os.walk(temp_dir):
        for filename in files:
            local_path = os.path.join(root, filename)
            relative_path = os.path.relpath(local_path, temp_dir)
            destination_s3_path = os.path.join(relative_path)

            logger.info("Uploading {} to bucket {}".format(destination_s3_path, destination_s3_cert_bucket))
            s3_client.upload_file(local_path, destination_s3_cert_bucket, destination_s3_path)

    return "Completed"

Zip up all the things

Take all the files that we just created and turn it into a Zip file that can used with Lambda.

cd ~/lambda
zip -r9 lets_encrypt_bundle.zip main.py letsencrypt.py zope
pushd ~/env/lib/python2.7/site-packages/ && zip -r9 ~/lambda/lets_encrypt_bundle.zip * && popd
pushd ~/env/lib64/python2.7/site-packages/ && zip -r9 ~/lambda/lets_encrypt_bundle.zip * && popd

Copy this Zip file (e.g SCP) to a local machine. After copying this off to a local machine, the EC2 instance used for this process can be shutdown. If the function needs to be further edited, you can just edit and repackage it into a zip on your local machine.

scp -i <key_name>.pem ec2-user@<public_ip>:lambda/lets_encrypt_bundle.zip .

Creating the Lambda Function

In the Lambda console, create a new Python function.

    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Action": [
            "Resource": "arn:aws:logs:*:*:*"
            "Effect": "Allow",
            "Action": [
            "Resource": [
            "Effect": "Allow",
            "Action": [
            "Resource": [
            "Effect": "Allow",
            "Action": [
            "Resource": [
            "Effect": "Allow",
            "Action": [
            "Resource": [

You now have an automated, server free, hands off process for renewing your Let’s Encrypt SSL certificate on CloudFront.

AWS Lambda Powered HTTP/SOCKS Web Proxy

October 20, 2016
proxy privacy golang aws lambda

SSL Pinning Workaround for Android Uber Apps

August 31, 2016
ssl mitm android uber notes