Encrypt All The Things

In recent years there’s been a movement in the tech community to use encryption whenever possible. On the web front, Google is a notable advocate of the importance of encrypting web traffic via HTTPS — it’s even started using HTTPS as a search ranking criteria. Secure web connection are needed because they protect the integrity of data transferred between browsers and servers, whereas a regular HTTP connection is vulnerable to eavesdropping and man-in-the-middle attacks.

Most browsers only display security information when a website uses HTTPS. A green lock indicates that the connection to a website is properly secured, while a red, crossed out lock indicates that a problem with the site’s configuration is making it insecure. If a website doesn’t use encryption at all, i.e. it uses plain HTTP, the browser simply shows a generic white icon:

Green LockRed LockWhite Page

Chrome browser icons for secure HTTPS, insecure HTTPS and regular HTTP connections

It’s quite obvious that something is wrong when a website displays the red lock; in fact, it is often accompanied by an error page describing the problem. Many users, however, are unaware that accessing websites over HTTP is also insecure. As such, there’s an initiative at the Chromium Project to inform users whenever they access content over insecure connections. Rather than showing a generic white icon, the Chrome Security Team proposed that HTTP websites show a security warning similar to the red lock above. Access to the site would still be permitted, but the user would be notified of potential security risks.

The security warning change is supposedly going to be a part of mainline Chrome in the near future, so I decided that it was probably time to configure HTTPS on my personal website. I chose to use Let’s Encrypt as my certificate authority for a variety of reasons, the foremost being cost. Most popular CAs such as Comodo, GoDaddy or Symantec charge fees when issuing and renewing SSL certs, however Let’s Encrypt is completely free. There are a few other free alternatives, but since LE is backed by Mozilla and Google the root certificate comes installed in most browsers.

Let’s Encrypt and Node.js

There are a variety of ways to install the Let’s Encrypt toolchain. The recommended way in the official guide is to simply clone the LE repo to a local directory. There’s also a Node package listed on npm — I haven’t tried this myself, but it seems to allow for tighter integration of Let’s Encrypt and Node applications. At the time of writing I was unable to find a LE package using APT on Ubuntu, but there was a package listed on the Debian web interface. Your mileage may vary on other Linux distributions.

To use Let’s Encrypt with Node, I ran it with the following command-line options:

./letsencrypt-auto certonly --standalone --email@example.com -d example.com -d www.example.com

Needless to say, example.com and email should replaced with real values. Following through all of the steps in the toolchain will produce four files, which are most likely placed in the directory /etc/letsencrypt/archive/example.com/.

The following code listing shows how to start an HTTPS server using the certificate files. The app passed in to the server can be any requestListener, such as an Express web app instance or a handler function. Note that I also start an HTTP server that redirects requests to the HTTPS server. This isn’t necessary/desirable in all cases, but it’s a simple way to force all incoming traffic to use the secure connection. If someone accidentally visits http://www.taylorpetrick.com, perhaps from a stale link, they’ll be redirected to https://www.taylorpetrick.com.

var http = require('http')
var https = require('https')

... 

http.createServer(function(req, res) {   
        res.writeHead(301, {"Location": "https://" + req.headers['host'] + req.url});
        res.end();
}).listen(80);

https.createServer({ 
        key: fs.readFileSync("/etc/letsencrypt/archive/example.com/privkey1.pem"),
        cert: fs.readFileSync("/etc/letsencrypt/archive/example.com/fullchain1.pem"),
        ca: fs.readFileSync("/etc/letsencrypt/archive/example.com/chain1.pem")
}, app).listen(443);

It may be more convenient to create sym links to the .pem files in a subdirectory of the web app and reference those, rather than specifying full file paths. I wouldn’t recommend copying the certificate files though — when it comes time to renew the certificate in four months, you’ll also have to remember to copy the new files to the web app. Using the full path or a sym link avoids this step.

SSL Labs Server Test

SSL Labs provides a free service that can be used to analyze the SSL configuration of a webserver. It performs a battery of tests and produces a detailed report that describes any configuration issues or vulnerabilities found during testing. The report also includes an letter grade that indicates how well the website did overall. When I first ran my personal website through the test I received a score of C due to some flaws in my server configuration; after fixing the issues and rerunning I improved my site score to an A. I figured I would document some of the fixes I made in case anyone else runs into the same problems.

SSL Report

SSL Labs report after a few hours of fixing config issues

Ciphers and Protocols

The first big “fix” was to update my Node installation to v5, as described in the Node package manager documentation. It turns out that older versions of Node use an outdated default cipher list. This can be changed manually via the ciphers option when creating the HTTPS server and by setting honorCipherOrder to true. See the Node TLS/SSL documentation for details.

The second change I made was to disable support for the SSL 2 and SSL 3 protocols. Websites should now exclusively use TLS 1.0-1.2, the successors to the deprecated SSL protocols. This can be enforced when creating the server in Node:

var constants = require('constants')
... 
https.createServer({
        secureOptions: constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_SSLv2,
        ... 
}, app).listen(443);

One thing to keep in mind is that disabling SSL 3 will also break support for Internet Explorer 6, since that browser lacks TLS support. The SSL Labs output will include a test error that indicates IE6 failed to access the server. For most web admins this won’t be a problem since very few people still use IE6, however it’s worth keeping in mind for larger sites with many users.

SSL v3 Error

Internet Explorer 6 fails to load the page once SSL protocols are disabled

DH Key Exchange

I also added Diffie-Hellman Key Exchange parameters to my configuration, as suggested by SSL Labs. This requires an extra file, which can be generated using the OpenSSL command line utility. SSL Labs recommended a 2048-bit DH parameter file:

openssl dhparam -out /etc/letsencrypt/archive/example.com/dh1.pem 2048

The file is then passed as an additional option when creating the HTTPS server:

https.createServer({
        ... 
        dhparam: fs.readFileSync("/etc/letsencrypt/archive/example.com/dh1.pem"),
}, app).listen(443);

Like the SSL 3 change, introducing DH can break compatibility with a few uncommon browsers. Almost all clients have proper support for 2048-bit parameters, however I noticed the following in my test report:

DH Key Error

An outdated Java HTTPS client doesn't support DH parameters with a size greater than 1024-bits

I decided to keep the 2048-bit parameter size anyway as it meets the SSL Labs and Node.js security recommendations.

HTTP Strict Transport Security

The last change I made to was to add HSTS headers. I chose to use helmet to add response headers:

var helmet = require('helmet')
... 
app.use(helmet.hsts({
      maxAge: 31536000000,
      includeSubdomains: true,
      force: true
}));

Getting an A+

The best possible score in the SSL Labs test is an A+. To get such a score, several other server side changes are necessary in addition to the ones list above. These include bumping the key and DH parameter size up to 4096 bits, adding SSL stapling and adding additional response headers. A lengthy guide on getting an A+ with Let’s Encrypt and Apache was posted on the LE discussion board. The guide also comes with a warning that explains that the changes will break compatibility with many older browsers — for this reason, I decided not to pursue an A+ grade and stuck with A.