Easy SSL Setup with Docker and Bud-tls

Easy TLS for any type of site.

heads up: this is a pretty old post, it may be outdated.

Adding SSL to your site is actually pretty easy, and as a good web citizen you owe it to the Internet. If the best interest of your visitors isn't a good enough reason for you, then you might want to add SSL because Google is using SSL as a signal of quality and SSL will be the default behavior of HTTP2.

It's actually easy, and doesn't require you to change much if you use an SSL terminator. This will allow you to develop your app without SSL. Many frameworks allow you to turn on SSL as an option, but that's going to be one level of difference between dev and prod. Also, SSL is a bottleneck and you're better off farming this out to a service that does it really well.

bud is a great SSL terminator. It's configurable via JSON, is very fast, and is full-featured.

Let's walkthrough a setup of bud with Docker. Docker does give you an extra layer: even if bud is hacked, they won't get into your app or server. You don't need your main app to be deployed with Docker, but if it already is, this will be even easier.

Get a cert

The first thing you need to do is appease the ridiculousness of the entrenched institutions and get an SSL certificate. Let's Encrypt makes it easy~ish and free. Follow the instructions, but here's the meat of what you need to do:

Generate a private key with:

openssl req \
 
      -newkey rsa:4096 -nodes -sha256 -keyout domain.key \
       -out domain.csr

Then, on a machine that already has DNS setup for the domain name you want to put under https:

 
# It assumes that /var/www/example is publicly accessible to the web
letsencrypt certonly --webroot -w /var/www/example -d example.com -d www.example.com

If you're just playing around, this great guide to all things OpenSSL will point you in the right direction to create a self-signed cert. This cert will not be accepted by any browser, but you can use it just to see how things work. If you're doing this, curl -k will be your friend :)

Once you have your keys, DO NOT EXPOSE THEM. Do not email them, put them on Dropbox, or in anyway don't let them leave your computer un-encrypted. If you do, the game is up. You'll need to revoke your cert and create a new one.

Generate a DH Key

You're going to enable perfect forward secrecy so that even if you site is somehow hacked, all your previous traffic will remain encrypted.

Run this, and keep the resulting file.

openssl dhparam -out dh.key 4096

Copy your cert to your sever

You need to ensure your key security, so we'll use ssl to copy things around.

ssh <yoursite>
# on the remote server
mkdir -p /srv/bud/keys
exit
# back on your computer
# copy the key file that you generated
scp cert.key <yoursite>:/srv/bud/keys/cert.key
# copy the public crt generated from Let's Encrypt (or your cert provider)
scp ssl-unified.crt <yoursite>:/srv/bud/keys/ssl-unified.crt
# copy your dh.key file
scp dh.key <yoursite>:/srv/bud/keys/dh.key

bud configuration

Bud is very nicely configured with a JSON file. You can configure if you want, but this a is a good default.

Make a new file with these contents and copy it up to the bud directory. It assumes you'll use docker to run bud. If you don't run your site with Docker, replace backend_port with the port your site runs at (probably 80) and backend_ip with the ip your site runs at (probably 0.0.0.0).

{
  "workers": 10,
  "restart_timeout": 250,
  "log": {
        "level": "notice",
        "facility": "user",
        "stdio": true,
        "syslog": true
    },
  "availability": {
        "death_timeout": 100000,
        "revive_interval": 25000,
        "retry_interval": 250,
        "max_retries": 500
    },
  "frontend": {
        "port": 443,
        "host": "0.0.0.0",
        "keepalive": 3600,
        "security": "tls12",
        "server_preference": true,
        "ssl3": false,
        "max_send_fragment": 1400,
        "allow_half_open": false,
        "npn": ["http/1.1", "http/1.0"],
        "ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA256:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA256:DHE-RSA-AES256-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA",
        "ecdh": "prime256v1",
        "cert": "/data/keys/ssl-unified.crt",
        "key": "/data/keys/cert.key",
        "dh": "/data/keys/dh.key",
        "passphrase": null,
        "request_cert": false
      },
  "balance": "roundrobin",
  "backend": [{
        "port": backend_port,
        "host": "backend_ip",
        "keepalive": 3600,
        "proxyline": false,
        "x-forward": true
      }]
}
scp bud.json <yoursite>:/srv/bud/bud.json

Install the bud docker image

ssh <yoursite>
sudo docker pull joeybaker/bud-tls
sudo docker rm -f bud
# if you run your site with docker
sudo docker run -d -v /srv/bud:/data -p 443:443 --name bud --link <your site docker container>:backend joeybaker/bud-tls
# if you don't run your site with docker
sudo docker run -d -v ~/bud:/data -p 443:443 joeybaker/bud-tls

Done! You have SSL on your site. Verify that things worked at SSLLabs. You should get an A+.

You can also confirm that you're not using SHA2, which is a broken security measure.

Things you can do to improve things

Enable Strict Transport Security Header

This tells modern browsers to prefer SSL. It's highly recommended. If you're running hapi, it's an easy setting to turn on called hsts.

Redirect all non-SSL traffic to SSL

For older browsers, the HSTS header isn't enough. You could use a simple redirector service to redirect all http traffic to https.

Or, this sample code for hapi.js demos how to redirect all HTTP traffic to HTTPS.

var config = {
 
   http: {
        uri: 'https://mysite.com'
        , ssl: true
    }
}
 
https:// force https and handle redirects
server.ext('onRequest', function serverOnRequest(req, next){
 
 https:// just let robots txt through, otherwise, google complains
  if (req.url.path === '/robots.txt'){
    next()
  }
  https:// enforce ssl
  else if (req.headers['x-forwarded-for'] && !req.raw.req.connection.xForward){
    server.log(['ssl', 'verbose'], 'init ssl')
    req.raw.req.connection.xForward = req.headers['x-forwarded-for']
    next()
  }
  else if (req.raw.req.connection.xForward){
    server.log(['ssl', 'verbose'], 'already ssl')
    https:// proceed fair lady!
    next()
  }
  else {
    if (config.http.ssl){
      server.log(['ssl', 'verbose'], 'redirect to SSL')
      next({statusCode: 301}).redirect(config.http.uri + req.url.path)
    }
    else next()
  }
})

Note: This post used to recommend using StartSSL to get your cert. That's a bad idea.