How to get A+ on the SSL Labs test in node.js

January 25, 2020

Update May 18 2015: CertSimple has added newer, even better ciphers into node.js itself, which appears in node 4. If you're using node 0.12, grab the cipher list from the latest node source which contains our changes, then add HSTS support per this article.

The SSL Labs test

The SSL Labs test examines a wide variety of aspects of HTTP servers, including simulating the handshakes where browsers and servers agree on crypto. It's a good starting point for checking your SSL configuration.

The test evolves over time - as new weaknesses get found in protocols, new technologies emerge that get added to requirements - so while the results here are accurate at publication, they might not be by the time you're reading this. Run your own tests and see.

Lastly: we're running this on node.js because a couple of the modules we use don't yet run on io.js. As you're about to see, io.js is a lot better out of the box than node 0.12 is. If you're using io.js, skip forward to the HSTS steps.

Introducing cipher suites

These are also called 'ciphersets' by node devs or just ciphers in the actual node function signatures.

Run the current stable node, 0.12. Start a TLS server. Statistically , it's likely you're using Express, so it will look something like this:

var server = https.createServer({
    key: privateKey,
    cert: certificate,
    ca: certificateAuthority
}, app);

The defaults for https.createServer() are described in the node docs. Expanding on that, you're actually running something like this.

var server = https.createServer({
    key: privateKey,
    cert: certificate,
    ca: certificateAuthority,
    ciphers: [
        "ECDHE-RSA-AES128-SHA256",
        "DHE-RSA-AES128-SHA256",
        "AES128-GCM-SHA256",
        "RC4",
        "HIGH",
        "!MD5",
        "!aNULL"
    ].join(':'),
}, app);

ciphers normally takes a string because that openssl expects, but we use an array and join it because it's a little easier to read, allows us to make comments above individual lines, etc.

Items in ciphers can take a few formats, including:

  • Full cipher suites, like AES128-GCM-SHA256. In wider SSL terms, OpenSSLs AES128-GCM-SHA256 is called TLS_RSA_WITH_AES_128_GCM_SHA256. We can convert the OpenSSL names into 'standard' names with the OpenSSL docs. This means:
    • Use RSA for key exchange. This is the famous RSA, public key cryptography that you probably already know about.
    • Use AES for the cipher algorithm - since public key crypto is slow, a symmetric algorithm (i.e., same password is used to encrypt and decrypt) is used to transfer the bulk of the data. That password, called a session key, was transmitted in the key exchange earlier.
      • Use 128 bit strength
      • Use GCM for the cipher mode - GCM also makes sure messages haven't been tampered with
      • Use SHA256 as input for the Pseudo Random Function. Note: originally we had this use of SHA256 as message authentication which wasn't correct - thanks to user 'masta' on Hacker News.
  • Wildcard entries like HIGH or RC4 that describe multiple cipher suites (again ,see the OpenSSL docs)
  • Blacklists using !, e.g. !MD5 since MD5 isn't considered secure anymore, since you can make two messages that have the same hash and that's been been used for real attacks on the server.

Now visit the SSL Labs test and test your server.

Out of the box, we've got some work to do.

Disabling RC4 and enforcing our preferred cipher order

OK, as SSL Labs notes, this server accepts the RC4 cipher, which is weak - there's more too, but we'll get to that later. Let's disable any cipher suite involving RC4 - using a ! just like the MD5 entry:

While we're at it, note this gem from the node docs:

honorCipherOrder : When choosing a cipher, use the server's preferences instead of the client preferences. Although, this option is disabled by default, it is recommended that you use this option in conjunction with the ciphers option to mitigate BEAST attacks.

It's somewhat worrying that the recommended option does not match the default. Let's use the recommended option:

// default node 0.12 ciphers with RC4 disabled
ciphers: [
    "ECDHE-RSA-AES128-SHA256",
    "DHE-RSA-AES128-SHA256",
    "AES128-GCM-SHA256",
    "!RC4", // RC4 be gone
    "HIGH",
    "!MD5",
    "!aNULL"
].join(':'),
honorCipherOrder: true

RC4 disabled, our grade is no longer capped at B. But something is broken with forward secrecy...

Fixing broken perfect forward secrecy

Perfect forward secrecy is rad. Even if your private key is stolen, the bad guys still need to get the session key of future conversations to read anything. That's amazing!

Since all crypto related items need cool logos now.

In the browser handshakes for Chrome 40 (which is already old at this point), Android 5.0.0 and Googlebot, you'll see 'No FS' in orange on the right. It turns out that Google product really seem to like AES128-GCM-SHA256 and picks it over the forward secrecy cipher suites (they start with an E for 'ephemeral'. This was found and fixed in io.js already.

Note the 'No FS' besides Google Chrome Desktop, Google Chrome on Android, and Googlebot

The same io.js pull request also sets honorCipherOrder to true by default.

Let's grab the ciphers from the iojs docs:

[
    "ECDHE-RSA-AES256-SHA384",
    "DHE-RSA-AES256-SHA384",
    "ECDHE-RSA-AES256-SHA256",
    "DHE-RSA-AES256-SHA256",
    "ECDHE-RSA-AES128-SHA256",
    "DHE-RSA-AES128-SHA256",
    "HIGH",
    "!aNULL",
    "!eNULL",
    "!EXPORT",
    "!DES",
    "!RC4",
    "!MD5",
    "!PSK",
    "!SRP",
    "!CAMELLIA"
].join(':'),

Using the iojs ciphers list fixes forward secrecy on Google products.

Finally (or firstly for io.js) using HSTS to tell browsers we expect to keep using HTTPS

For an A+ we need HSTS. HSTS tells clients using HTTPS to expect to keep using HTTPS for some time in future - which makes attacks that try and downgrade HTTPS to HTTP harder. For SSL Labs explicitly, the want at least six months ahead - which should be fine, since we plan to keep using HTTPS for a very long time.

Setting up HSTS only takes a moment. Grab the helmet module from npm. Assuming Express (because Express is popular), in app.js, it looks like:

var helmet = require('helmet');

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

And re test:

Boom.

Summary

node.js 0.12:

These issues have been resolved in io.js. Even if you can't use io.js yet, you can still use the io.js cipher suite settings to resolve them.

Both io.js and node.js 0.12:

  • Need HSTS for an A+ score. This is a one minute fix using the helmet module.

Here's a node 0.12 default Express app modified to get A+ on the SSL Labs test.