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.
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.
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
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.
docker run \
--rm \
-v "/etc/letsencrypt:/etc/letsencrypt/" \
-v "$HOME/.aws/:/root/.aws:ro" \
-e AWS_PROFILE \
-e AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY \
danihodovic/certbot-route53 -d 'mydomain.com' -d '*.mydomain.com' -m [email protected]
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'
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
ssl_private_key_file_path
- the path to the private key. Certbot places this
in /etc/letsencrypt/live/mydomain.com/privkey.pem
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"
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"]
}
}
It's a good idea to backup the certificates generated in /etc/letsencrypt
on
your local machine incase your hard disk fails, but I won't be covering that in
this post.