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 Jekyll 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.

Overview

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

  • Launch a temporary EC2 instance using the official Amazon Linux AMI
  • On this instance, create a Python virtualenv with all required dependencies
  • Write a Python function that will execute the Let’s Encrypt CLI
  • Add both Python function and dependencies into a single Zip file
  • Create a Lambda function using this Zip file
  • Schedule it to run every 30 days

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 \
       ca-certificates

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()
logger.setLevel(logging.INFO)

# 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
    try:
        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))
    else:
        logger.info(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"
EOF

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.

  • Pick ‘Upload a .ZIP file’ option and choose ‘lets_encrypt_bundle.zip’.
  • For handler, enter ‘main.handler’.
  • For role, click new ‘Basic execution role’.
  • In pop-up window for role, give a name for the new role and use below configuration. Make sure to replace S3 bucket names with your own.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:UploadServerCertificate",
                "iam:UpdateServerCertificate",
                "iam:DeleteServerCertificate"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectACL"
            ],
            "Resource": [
                "arn:aws:s3:::yourdomain.com/*",
                "arn:aws:s3:::yourdomain.com-certs/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::yourdomain.com/.well-known/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistributionConfig",
                "cloudfront:UpdateDistribution"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
  • Set timeout to a minute and create the function.
  • In Lambda console, select ‘Event sources’ tab and click ‘Add event source’.
  • In event source pop-up window, choose ‘Scheduled Event’ and setup monthly cron, for example rate(30 days).

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