Set up Heroku SSL using Let's Encrypt

Set up Heroku SSL using Let's Encrypt

You built your first app on Heroku and want to set up a custom domain. Everything is going well until you realize you need to serve your website over https. Unfortunately Heroku does not integrate with LE (yet) so you'll have to do some manual labor. Lucky for you, it's easy to do so!

How Let's Encrypt Verifies Your Domain

LE will make a GET request to an endpoint like https://YOURDOMAIN.com/.well-known/acme-challenge/KEY. It will expect this endpoint to respond with KEY.TOKEN. The response body should only contain plain text (no formatting, no spaces).

Setting up The Endpoint

I'll be using Node.js + Express in this example, but you can do the same in your language/framework of choice. Place this near the top of where your Express app is instantiated.

if (process.env.LE_URL && process.env.LE_CONTENT) {
  app.get(process.env.LE_URL, function(req, res) {
    return res.send(process.env.LE_CONTENT)
  });
}

Notice how we are not hard-coding the .well-known/acme-challenge/ part of the URL as that may change.

Installing Certbot

Certbot is the official LE client developed by EFF.

The easiest way to install it on a Mac is using homebrew:

brew install certbot

On Ubuntu:

sudo apt-get install certbot

Using Certbot

We will be running it in manual mode, since we don't want it to install the certificates on our development machine.

certbot certonly --manual

This will probably require sudo privileges, but you can set --logs-dir, --config-dir, and --work-dir to writeable paths.

Follow the prompts until it displays something like this:

Make sure your web server displays the following content at https://YOURDOMAIN.com/.well-known/acme-challenge/1a2b3c before continuing:

1a2b3c.9z8x7y

Press ENTER to continue.

Setting Environment Variables in Heroku

Now that our endpoint is set up, all we have to do is set the config vars in Heroku.

heroku config:set LE_URL=/.well-known/acme-challenge/1a2b3c LE_CONTENT=1a2b3c.9z8x7y

You can also do this in the app settings page.

Verifying Your Domain

Once your endpoint is pushed to Heroku and the config vars are set, press enter/return in the Certbot terminal prompt to have LE send the GET request and verify your domain.

Installing the Certificates

If all goes well, your certificates should be issued and placed in /etc/letsencrypt.

sudo tree /etc/letsencrypt

Should look something like this:

/etc/letsencrypt
├── accounts
│   └── acme-v01.api.letsencrypt.org
│       └── directory
│           └── b2aa2f55bd4h3c65f2aac2bbb522d0c
│               ├── meta.json
│               ├── private_key.json
│               └── regr.json
├── archive
│   └── YOURDOMAIN.com
│       ├── cert1.pem
│       ├── chain1.pem
│       ├── fullchain1.pem
│       └── privkey1.pem
├── csr
│   └── 0000_csr-certbot.pem
├── keys
│   └── 0000_key-certbot.pem
├── live
│   └── YOURDOMAIN.com
│       ├── cert.pem -> ../../archive/YOURDOMAIN.com/cert1.pem
│       ├── chain.pem -> ../../archive/YOURDOMAIN.com/chain1.pem
│       ├── fullchain.pem -> ../../archive/YOURDOMAIN.com/fullchain1.pem
│       └── privkey.pem -> ../../archive/YOURDOMAIN.com/privkey1.pem
└── renewal
    └── YOURDOMAIN.com.conf

To add them to heroku:

heroku certs:add /etc/letsencrypt/live/YOURDOMAIN.com/fullchain.pem /etc/letsencrypt/live/YOURDOMAIN.com/privkey.pem

HTTPS Only

To redirect users to the https version of your site, you can use the x-forwarded-proto header set by Heroku.

if (process.env.NODE_ENV === 'production') {
  app.use(function(req, res, next) {
    if (req.headers['x-forwarded-proto'] !== 'https' && req.path !== process.env.LE_URL) {
      return res.redirect(['https://', req.get('Host'), req.url].join(''));
    }
    return next();
  });
}