This post was written at a time when AWS certificates took forever to generate. Nowadays the process is much faster, so use ACM if you're already on AWS. ACM also automatically renews your certificates.

Introduction

Google Chrome will start marking HTTP sites as 'not secure' by July 2018. This is as good time as ever to secure our sites. If you happen to be deploying your site on AWS and provision your infrastructure using Terraform there is a simple way to generate certificates using Terraform and LetsEncrypt.

LetsEncrypt is an open and free Certificate Authority (CA) provided by the Internet Security Research Group (ISRG). LetsEncrypt provides software tools to automatically generate TLS certificates and verifies said certificates. Certbot is a client side tool developed by LetsEncrypt that allows a developer to automatically generate a TLS certificate for a domain.

Terraform is a tool for provisioning infrastructure. It allows us to describe our infrastructure resources as code and execute it to create our infrastructure in the cloud.

AWS already provides a way to generate SSL certificates by using their CA. [AWS ACM] (https://aws.amazon.com/certificate-manager/) integrates nicely with existing AWS services. It's a good option if you're already running on AWS. However when I was using it I found that the time to create and validate a certificate using ACM was very slow. Usually it would take between 30-60 minutes for me to validate a certificate which is too long for me.

On the flip side LetsEncrypt also integrates nicely with AWS using DNS validation . By creating route53 records using the certbot DNS plugin we can generate wildcard certificates for our domain and all of the subdomains. I found validation using LetsEncrypt to be much faster than AWS ACM. The validation process takes less than a minute and generates certificates to your local machine.

The benefit AWS ACM has over LetsEncrypt is that it will automatically renew your certificates as long as they're being actively used by your domain and an AWS service, such as a ELB or Cloudfront.

Implementation

Creating our Route53 hosted zone

First of all we will need to create a route53 hosted zone. This is where the A records for our domain will reside and also where certbot will create the DNS TXT records.

variable "domain" { type = "string" } resource "aws_route53_zone" "main" { name = "${var.domain}" } output "name_servers" { value = "${aws_route53_zone.main.name_servers}" }

We also need to provide variables for Terraform in a terraform.tfvars file.

domain = "mydomain.com"

Run terraform to create the hosted zone

terraform apply -auto-approve

At this point we need to go to our registrar and change the authoritative DNS Name Servers for our domain. Retrieve the name servers from our new route53 zone using Terraform output:

$ terraform output name_servers ns-1153.awsdns-16.org, ns-170.awsdns-21.com, ns-1942.awsdns-50.co.uk, ns-664.awsdns-19.net

Now take these NS records and enter them as the authoritative nameservers for your domain in your registrar. These changes can take from minutes to days to propagate, but for domains I have purchased at Namecheap, Register365 and Route53 it has usually only taken a few minutes.

To ensure that our domain is using our Route53 name servers you can run the whois command that's available on most platforms.

$ whois danihodovic.com | grep 'Name Server' Name Server: NS-1133.AWSDNS-13.ORG Name Server: NS-1849.AWSDNS-39.CO.UK Name Server: NS-443.AWSDNS-55.COM Name Server: NS-858.AWSDNS-43.NET

Generating our certificate

We're going to use LetsEncrypt with the Route 53 plugin. to generate a TLS certificate on our local machine.

LetsEncrypt's client software, Certbot, will create a TXT record with a random token in our hosted zone. It will proceed to tell LetsEncrypt to query the automotive servers for our domain. LetsEncrypt will look for the TXT records Certbot created in our domain and find the random token which proves ownership of the domain.

Certbot requires AWS credentials and permissions to create route53 records. An example of what IAM policies are required can be found here. If you're using AWS credentials with administrator access you don't have to worry about IAM.

It's time to run Certbot and generate the certificate. I've created a Dockerfile which packages Certbot and the Route53 DNS plugin so that I don't have to install it on my local machine. If you're already using Docker you can run the following command.

Here is the non-docker version (Ubuntu is assumed to be the distribution).

# Install certbot sudo add-apt-repository ppa:certbot/certbot sudo apt-get update sudo apt-get install certbot # Install the route53 plugin sudo apt-get install python-pip sudo pip install certbot-dns-route53 certbot certonly \ -n \ --agree-tos \ --email [email protected] \ --dns-route53 \ -d 'mydomain.com' \ -d '*.mydomain.com'

Creating an aws_iam_certificate resource

Now we'll upload the certificate LetsEncrypt generated using the aws_iam_server_certificate resource in Terraform. This allows other AWS resources, such as ELBs and Cloudfront Distributions to use our certificate to encrypt traffic between end users and the ELB or Cloudfront.

We'll use three new variables for the IAM certificate. These are paths to the files created by Certbot.

ssl_cert_file_path - the path to the certificate file. Certbot places this in /etc/letsencrypt/live/mydomain.com/cert.pem

- the path to the certificate file. Certbot places this in ssl_private_key_file_path - the path to the private key. Certbot places this in /etc/letsencrypt/live/mydomain.com/privkey.pem

- the path to the private key. Certbot places this in ssl_certificate_chain_file_path - the path to the certificate chain. Certbot places this in /etc/letsencrypt/live/mydomain.com/chain.pem

variable "ssl_cert_file_path" { type = "string" } variable "ssl_private_key_file_path" { type = "string" } variable "ssl_certificate_chain_file_path" { type = "string" } resource "aws_iam_server_certificate" "cert" { name_prefix = "${var.domain}" certificate_body = "${file(pathexpand(var.ssl_cert_file_path))}" private_key = "${file(pathexpand(var.ssl_private_key_file_path))}" certificate_chain = "${file(pathexpand(var.ssl_certificate_chain_file_path))}" # Some properties of an IAM Server Certificates cannot be updated while they # are in use. In order for Terraform to effectively manage a Certificate in # this situation, it is recommended you utilize the name_prefix attribute and # enable the create_before_destroy lifecycle block. lifecycle { create_before_destroy = true } }

We'll populate these variables in terraform.tfvars .

domain = "mydomain.com" ssl_cert_file_path = "/etc/letsencrypt/live/mydomain.com/cert.pem" ssl_private_key_file_path = "/etc/letsencrypt/live/mydomain.com/privkey.pem" ssl_certificate_chain_file_path = "/etc/letsencrypt/live/mydomain.com/chain.pem"

Using our aws_iam_certificate

We can now use the IAM certificate to encrypt traffic between our load balancer and our visitors.

resource "aws_elb" "main" { listener { instance_port = 80 instance_protocol = "http" lb_port = 80 lb_protocol = "http" } listener { instance_port = 443 instance_protocol = "http" lb_port = 443 lb_protocol = "https" ssl_certificate_id = "${aws_iam_server_certificate.cert.arn}" } security_groups = [ "${aws_security_group.elb.id}" ] }

The ELB also needs a security group that allows for incoming and outgoing traffic.

resource "aws_security_group" "elb" { ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } egress { from_port = 0 to_port = 65535 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } }