29 December 2015

Getting a Let's Encrypt certificate up and running on a static Amazon AWS Cloudfront website and on Express

The Problem

Setting up an HTTPS website is generally a pain. One must acquire a certificate for use with TLS (not trivial) and then monkey around with getting the pieces in the correct format for the host technology.

When we first set up our site, the process took hours due to delays from the Certificate Authority (CA).

The other issue with CAs is that most of them do not issue free certificates.

Since we use and recommend using static websites hosted on Amazon AWS S3 Static Website Hosting using Amazon CloudFront CDN as the Content Delivery Network (CDN) with dynamic server endpoints for serving dynamic data, capturing analytics, performing business logic, etc. (we normally do this via Express on Node.js), we were interested in finding a way to quickly generate certificates and, if possible, a way that is free. 

Let's Encrypt

Let's Encrypt is a free, automated and open certificate authority. It issues 90-day certificates that can be auto-renewed via Automated Certificate Management Environment (ACME).

The technology is in beta, and unfortunately, the tools are flaky on Amazon EC2 Linux boxes. Also, the tools appear to be designed mostly for generating certificates on the same machine as the website host (not our use case). 

Let's Encrypt without sudo

Thankfully, an apparently clever chap named Daniel Roesler has created Let's Encrypt without sudo that with a bit of hacking works well for us.

CloudFront Script

First, create a directory  for the site (we do ours as a directory within the "letsencrypt-nosudo" directory).

Next, run this script and follow all of the instructions EXCEPT for the one to start a local server.

openssl genrsa 2048 > user.key && \
openssl rsa -in user.key -pubout > user.pub && \
openssl genrsa 2048 > domain.key && \
openssl req -new -sha256 -key domain.key -subj "/CN=www.YOUR_DOMAIN_NAME.com" > ./domain.csr && \
python ../sign_csr.py --public-key ./user.pub ./domain.csr > ./domain.crt && \
wget https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem

When you get to the server step, you will have in the prompt the pieces you need to upload the ACME file to AWS S3.

Suppose the prompt has the following in the "write" statement:

x0Cgs-62meybGnAco2rExV0y_WS1RRVwhTBBRAfaSFQ.PUN4jJ4AfNuF7Al_u6cA-dVqfLtpKYPm9LVxVEvXcao.

To pass the Let's Encrypt ACME verification, you will need to create a file named 62meybGnAco2rExV0y_WS1RRVwhTBBRAfaSFQ with the following as the contents:

x0Cgs-62meybGnAco2rExV0y_WS1RRVwhTBBRAfaSFQ.PUN4jJ4AfNuF7Al_u6cA-dVqfLtpKYPm9LVxVEvXcao

in the path YOUR_BUCKET/.well-known/acme-challenge/.

The file should be uploaded with the Content-Type: text/plain.


Once the file is uploaded and you have verified access to it. Hit <ENTER> to let Let's Encrypt verify the ACME challenge.

Once the scripts have completed, you will need to run the following command (assuming you have the AWS CLI tools installed and have your keys stored as environment variables):

aws iam upload-server-certificate \
--server-certificate-name  www.YOUR_DOMAIN.com \
--certificate-body file://domain.crt \
--private-key file://domain.key \
--certificate-chain file://lets-encrypt-x1-cross-signed.pem \
--path /cloudfront/

Express Script

Follow the steps as outlined, but start the server when prompted.

For Express, you will need a slightly different chain. Run the following command:

cat domain.crt lets-encrypt-x1-cross-signed.pem > chained.pem

Then in your node.js app (we use a custom logging script; comment out those lines if you don't have a logging module),

try {
  var fs = require("fs");
  var key = fs.readFileSync("YOUR_PATH/domain.key");
  cert = fs.readFileSync("YOUR_PATH/chained.pem");
  var https_options = {
    key: key,
    cert: cert,
  };
  var tls = https.createServer(https_options, app).listen(5001, function() {
    var host = tls.address().address;
    var port = tls.address().port;
    logger.info('App listening at https://%s:%s', host, port);
  });
}
catch (e) {
  logger.error(e);
}

No comments: