There are many wordy articles on configuring your web server’s TLS ciphers. This is not one of them. Instead I will share a configuration which is both compatible enough for today’s needs and scores a straight “A” on Qualys’s SSL Server Test.

Disclaimer: I’m updating this post continually in order to represent what I consider the best practice in the moment – there are way too many dangerously outdated articles about TLS-deployment out there already.

Therefore it may be a good idea to check back from time to time because the crypto landscape is changing pretty quickly at the moment. You can follow me on Twitter to get notified about noteworthy changes.

If you find any factual problems, please reach out to me and I will fix it ASAP.

Quick update: Qualys has tightened the screws and to get an “A”, you have to disable TLS 1.0 and TLS 1.1. I’ve been recommending that in the “Next Steps” section for a while so now consider them the main instructions until I find the time to rewrite the whole article (that will also add TLS 1.3 support).

The main part that follows is still relevant if you need to support older browsers. Scroll down if that’s not the case for you.

Rationale

If you configure a web server’s TLS configuration, you have primarily to take care of three things:

disable SSL 2.0 (FUBAR) and SSL 3.0 (POODLE), disable TLS 1.0 compression (CRIME), disable weak ciphers (DES/3DES, RC4), prefer modern ciphers (AES, ChaCha20), modes (GCM), and protocols (TLS 1.2).

You should also put effort into mitigating BREACH. That’s out of scope here though as it’s largely application-dependent.

Software and Versions

On the server side you should update your OpenSSL to 1.0.1c+ so you can support TLS 1.2, GCM, and ECDHE as soon as possible. Fortunately that’s already the case in Ubuntu 12.04 LTS and later.

On the client side the browser vendors are starting to catch up. As of now, Chrome 30, Internet Explorer 11 on Windows 8, Safari 7 on OS X 10.9, and Firefox 26 all support TLS 1.2.

RC4

There used to be a bullet point suggesting to use RC4 to avoid BEAST and Lucky Thirteen. And ironically that used to be the original reason for this article: when Lucky Thirteen came out the word in the streets was: “use RC4 to mitigate” and everyone was like “how!?”.

Unfortunately shortly thereafter RC4 was found broken in a way that makes deploying TLS with it nowadays a risk. While BEAST et al require an active attack on the browser of the victim, passive attacks on RC4 ciphertext are getting stronger every day. In other words: it’s possible that it will become feasible to decrypt intercepted RC4 traffic eventually and the NSA probably already is. Microsoft even issued a security advisory that recommends to disable RC4. As of 2015, there’s also an RFC.

The String

ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM

You can test it against your OpenSSL installation using

openssl ciphers -v 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM'

to see what’s supported.

You’ll get:

Best possible encryption in all browsers.

Perfect forward secrecy; if your web server, your OpenSSL, and their browser support it.

It doesn’t offer RC4 even as a fallback. Although its inclusion at the end of the cipher string shouldn’t matter, active downgrade attacks on SSL/TLS exist and having RC4 as part of the the cipher string you potentially expose all of your users to it.

Same goes for 3DES as of SWEET32.

Using ECDSA for authentication is even as of 2015 still very rare and cumbersome/expensive to deploy. Hence it’s completely ignored by the string. That makes it being sorted after RSA which is probably not what you want if you went the lengths to obtain an EC certificate. However if you manage to deploy a dual-certificate setup (which you still need nowadays), you probably don’t need this article and it would have made the string twice as long.

The string also prefers AES-256 over AES-128 (except for GCM which is preferred over everything else). It does so mostly for liability reasons because customers may insist on it for bogus reasons.

However quoth a cryptographer:

AES-128 isn’t really worse than AES-anythingelse, at least not in ways you care about

The very simplified gist here is that the only reason for having 256 bit keys are quantum computers which are less likely to become a problem than the key scheduling issues in AES-256. But let me stress that both are fine. It’s just that adding AES-256 ciphers doesn’t improve your security in practice.

So if AES-128 is fine for you, feel free to add an ‘ :!AES256 ’ to the end of the cipher string.

Apache

SSLProtocol ALL -SSLv2 -SSLv3 SSLHonorCipherOrder On SSLCipherSuite ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM

This works on both Apache 2.2 and 2.4. If your OpenSSL doesn’t support the preferred modern ciphers, it will fall back gracefully but your configuration is ready for the future.

Please note: you need Apache 2.4 for ECDHE and ECDSA. You can circumvent that limitation by putting an SSL proxy like hitch or even nginx in front of it and let Apache serve only plain HTTP.

TLS compression is a bit more complicated: as of Apache 2.2.23, it’s not possible to switch it off inside of Apache. For Apache 2.2.24+ and 2.4.3+ you can switch it off using:

SSLCompression Off

Currently the default is On . But that changed from 2.4.4 on.

The good news for Ubuntu admins is that Ubuntu has back ported that option into their 2.2 packages – and set it to off by default – so you should be fine. On RHEL/CentOS you used to have to set an environment variable but that has been fixed too and should be correct by default now.

nginx

ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers On; ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM;

TLS compression depends on the version of nginx and the version of OpenSSL. If OpenSSL 1.0.0 or later is installed, anything after nginx 1.0.9 and 1.1.6 is fine. If an older OpenSSL is installed, you’ll need at least nginx 1.2.2 or 1.3.2.

Have a look at this serverfault answer for more details.

TL;DR on TLS compression & nginx: if you’re using Ubuntu 12.04 LTS or later you’re fine (OpenSSL 1.0.1/nginx 1.1.19).

CentOS/Red Hat Enterprise Linux 6

Although CentOS 6.5 is shipping an OpenSSL that is capable of ECDHE key exchange, it doesn’t ship an nginx and the nginx you get from nginx.org is compiled against an older OpenSSL. Therefore openssl ciphers will confusingly show you all the shiny ciphers but nginx just won’t offer it to the clients.

That’s terrible because it costs you PFS for IE browsers.

You’ll have to compile nginx yourself or source it from an alternative package index (EPEL for instance).

HAProxy

For TLS matters HAProxy shouldn’t be older than version 1.5.

global ssl-default-bind-options no-sslv3 ssl-default-bind-ciphers ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM ssl-default-server-options no-sslv3 ssl-default-server-ciphers ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM tune.ssl.default-dh-param 2048

Bonus Points

Qualys updated their requirements on 2014-01-21 and the cipher suites here are still “A”–material. If you want an “A+” though, you’ll need to tweak your HTTP headers too. This goes way beyond the scope of this article, check out securityheaders.io to get started.

Next Steps

Depending on your target audience, you can take things further.

“I only need to support web browsers.”

If your server is only frequented by web browsers and you don’t have to fear curl calls from underpowered routers that disable DHE in their SSL stack for performance reasons, you can forbid RSA key exchange for good by appending a :!RSA at the end of your cipher string.

This gives you always forward secrecy with the weakest key exchange being DHE. Therefore no amount of protocol downgrade attacks can force you into a non-PFS connection.

If you can, I highly recommend doing this.

“I only care about web browsers released in the past five years.”

Now things get interesting. If you’re in this luxurious situation you can allow only great ciphers and let the browser pick. This is especially interesting once ChaCha20 comes into play (OpenSSL 1.1+) which is the currently fastest encryption algorithm if no AES hardware acceleration is available – a quite common situation on smartphones.

To make most of the next instructions you’re gonna need a recent server/OpenSSL stack but you should get good results on Ubuntu 12.04 LTS already:

Permit TLS 1.2 only. Disable server side ciphers. Use the following cipher string: ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM

Apache

SSLProtocol -ALL +TLSv1.2 SSLHonorCipherOrder Off SSLCipherSuite ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM

nginx

ssl_protocols TLSv1.2; ssl_prefer_server_ciphers Off; ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;

HAProxy

global ssl-default-bind-options force-tlsv12 prefer-client-ciphers ssl-default-bind-ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM ssl-default-server-options force-tlsv12 ssl-default-server-ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM

Summary

This gives you always the fast and forward-secure ECDHE for key exchange and the pretty good ECDHE-RSA-AES128-SHA256 becomes the weakest cipher. Since the browser picks the cipher now, it also perfectly works with ECDSA out of the box.

The price is that your server now requires at least Android 4.4 (2013), IE 11 (2013), Safari 6 for iOS 6 (2012), Safari 7 for macOS 10.9 (2013), Java 8 (2014), and you’re blocking the current version (as of 2018: 2015) of the Baidu bot. Chrome and Firefox have been supporting good TLS for so long and forced rolling updates on their users, that it’s not even worth it to investigate.

The increase in security is not as interesting as with dropping RSA, however it feels really good and shorter cipher lists also have a slight performance advantage.

Finally

Make sure to test your server afterwards! A common problem nowadays are weak DH parameters – please refer to this guide on how to fix that if you use DHE. Sadly, except for HAProxy, it’s a bit more involved than just setting an option.

If you want to learn more about deploying SSL/TLS, Qualys’s SSL/TLS Deployment Best Practices are a decent primer.

Since I wrote this article in 2013, many TLS checkers appeared. Mozilla even built an observatory that aggregates their results.

However their results vary and sometimes even contradict each other so I recommend to focus on one. In my case it’s the Qualys for TLS and securityheaders.io for headers.

For investigating the SSL/TLS behavior of your browser, Qualys also has a page for that.

History

I’ve been keeping this guide up to date since 2013 throughout the very tumultous evolution of modern TLS (as the following history shows). It would be great if you’d consider a small token of appreciation – no account required!

‌2020-03-23 : Added that you have to use the best cipher on the page to get an A. I will do more updates when I find the time.

: Added that you have to use the best cipher on the page to get an A. I will do more updates when I find the time. ‌2019-06-23 : Disabled AESCCM which is safe but uncommon and slow.

: Disabled which is safe but uncommon and slow. 2018-10-04 : Small fix to the HAProxy next steps config: prefer-client-ciphers is neither needed nor allowed in ssl-default-server-options .

: Small fix to the HAProxy next steps config: is neither needed nor allowed in . 2018-08-09 : Added next steps to drop RSA, and going ECDHE-only. Added ChaCha20 to the default string for the case a client supports ChaCha20 but no AES-GCM (I’m not aware of any though).

: Added next steps to drop RSA, and going ECDHE-only. Added ChaCha20 to the default string for the case a client supports ChaCha20 but no AES-GCM (I’m not aware of any though). 2017-06-12 : Added DHE param size to HAProxy.

: Added DHE param size to HAProxy. 2016-08-24 : Removed 3DES because of SWEET32. This drops IE 8 on Windows XP support which shouldn’t be a concern in 2016.

: Removed 3DES because of SWEET32. This drops IE 8 on Windows XP support which shouldn’t be a concern in 2016. 2015-05-20 : The new weakdh/Logjam attack doesn’t affect you if you followed these instructions. It might be worthwhile though to create your own DH groups with at least 2048 bits as described in this guide.

: The new weakdh/Logjam attack doesn’t affect you if you followed these instructions. It might be worthwhile though to create your own DH groups with at least 2048 bits as described in this guide. 2015-01-16 : Added a note on ECDSA because there seemed to be some confusion about it.

: Added a note on ECDSA because there seemed to be some confusion about it. 2014-11-25 : Added HAProxy, courtesy of Sander Klein.

: Added HAProxy, courtesy of Sander Klein. 2014-10-24 : Updated the TLS compression part about Red Hat/CentOS. TL;DR: it’s secure by default now.

: Updated the TLS compression part about Red Hat/CentOS. TL;DR: it’s secure by default now. 2014-10-21: Clarified that Internet Explorer 8 on Windows XP works fine with TLSv1-only. The original wrong claim stemmed from the fact that I double-checked using SauceLabs and it turned out that they don’t use Windows XP when you ask for it but Windows Server 2003 R2 instead. Since then I re-tested using actual Windows XP workstations running Internet Explorer 8 and it works fine.

2014-10-15: Disabled SSLv3 because of POODLE.

2014-04-11 : Added note on CentOS/RHEL 6 and nginx+ECDHE.

: Added note on CentOS/RHEL 6 and nginx+ECDHE. 2014-01-17: RSA+AES has been split into RSA+AESGCM:RSA+AES . This is a very minor update that only matters if you have TLS 1.2 but neither ECDHE nor DHE (which is rather rare). It makes sure, that RSA-AES-128-GCM is preferred over RSA-AES-256-CBC in these cases. Since both are just fallbacks and you should use PFS ciphers, this is just minutiae.

Credits: The initial version of this article has been kindly proof-read by Christian Heimes. My current cipher string is based on one by Zooko Wilcox-O’Hearn who contrary to me is a cryptographer and has just launched a great secure cloud storage you should check out. The errors are all still mine.