Hardening Your Web Server’s SSL Ciphers

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.


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

  1. disable SSL 2.0 (FUBAR) and SSL 3.01 (POODLE),
  2. disable TLS 1.0 compression (CRIME),
  3. disable weak ciphers (DES/3DES, RC4), prefer modern ciphers (AES), 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 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.


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 even an RFC.

The String


You can test it against your OpenSSL installation using


to see what’s supported2.

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 certificate3. 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 like I do.


SSLProtocol ALL -SSLv2 -SSLv3
SSLHonorCipherOrder On

This works on both Apache 2.2 and 2.4. If your OpenSSL doesn’t support the preferred modern ciphers (like the still common 0.9.8), 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.


ssl_prefer_server_ciphers On;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

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 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).


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

   ssl-default-bind-options no-sslv3

   ssl-default-server-options no-sslv3
   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.


Make sure to test your server afterwards!

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.


Since I’m keeping this up to date, I’m going to document changes for returning visitors:

  • 2017-06-12: 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.
  • 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.
  • 2015-01-16: Added a note on ECDSA because there seemed to be some confusion about it.
  • 2014-11-25: 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.
  • 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.
  • 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.

I spoke at at multiple Python conferences about the Sorry State of SSL; you may want to check out the talk page and video!

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.

  1. This will cut you off Internet Explorer 6 on Windows XP but you should do it anyway. IE 8, Chrome, and Firefox run just fine on XP. ↩︎
  2. The !DSS at the end is only for cosmetic reasons to keep the output of openssl ciphers shorter. It has no implication security-wise because the signature-algorithm is determined by the type of your server key. DSS is not commonly deployed in the wild and if you don’t want your server to use it, simply don’t use a DSS server key (which is very unlikely to happen anyway). The current widespread standard is RSA while ECDSA is trying to get traction. ↩︎
  3. Please don’t conflate RSA for authentication (which is okay) with RSA for key exchange (which is terrible because it lacks PFS). ↩︎