Phasing out SSL / TLS protocols and ciphers with user-friendly error messages

The phasing out of legacy encryption protocols like TLS 1.0 or the family of CBC encryption ciphers is a recurring necessity. This has been going on for many years. And it will only be a few years until TLS 1.2 should be retired in favor of its successor TLS 1.3.

When disabling one of the SSL/TLS protocols or retiring a cipher suite, then there is always the question of how many users you are blocking. If you have configured your server side encryption endpoint with an extended access log, then such a report is at your fingertips. I have described such a setup in my tutorial on the topic. So you can take an educated decision at the right moment when the user base is no longer substantial. The browser vendors are doing a very good job updating their installations. So the numbers are generally coming down a lot faster then they used to. But maybe you have legacy agents that access certain APIs and locking them out is not an option. So you better know who is using the old cipher before you disable it.

However, there is a problem that persists: When you disable a protocol or cipher, then the users relying on it can no longer connect to your server. They are confronted with a hard to understand error message. You do not have control over this error message and for 99% of the users it will look very bad. For the users, it will look as if you had a security problem – and not they with their outdated browser.

Would not it be better to introduce a grace period during which you still accept the connection, but redirect it to a dedicated “weak cipher” page? On said page you could then explain the problem and urge the user to upgrade the browser.

It is surprisingly simple to pull this off with Apache.

Six steps for user-friendly error pages

Step 1 : Create an error page and name it weak-encryption.html.

Step 2 : Define the legacy SSL / TLS protocols and ciphers that you want
to phase out.

Step 3 : Configure a conditional statement (-> <If> ) in your Apache configuration that checks the SSL/TLS protocol used for the connection.

Step 4 : Write a nested conditional statement that checks if the URI is different from weak-encryption.html.

Step 5 : If the condition in step 3 and the condition in step 4 match, then
create a rewrite rule that redirects to the weak-encryption path.

Step 6 : Repeat step 3 to 5 for weak ciphers.

An Apache recipe to put this in practice

I’m a proposing a configuration that puts this into action. It works with the help of the Apache <If> directive and ModRewrite:

<If "%{SSL_PROTOCOL} =~ /^(TLSv1.0|TLSv1.1)$/" >   <If "%{REQUEST_URI} != '/weak-encryption.html'" >     RewriteRule  ^/  /weak-encryption.html  [redirect,last]   </If> </If> <If "%{SSL_CIPHER} =~ /^(AES128-SHA|AES256-SHA|DHE-RSA-AES128-SHA|DHE-RSA-AES256-SHA|ECDHE-RSA-AES128-SHA|ECDHE-RSA-AES256-SHA|AES128-SHA256|AES256-SHA256|DHE-RSA-AES128-SHA256|DHE-RSA-AES256-SHA256|ECDHE-RSA-AES128-SHA256|ECDHE-RSA-AES256-SHA384)$/" >   <If "%{REQUEST_URI} != '/weak-encryption.html'" >     RewriteRule  ^/  /weak-encryption.html  [redirect,last]   </If> </If>

This is configured on The site works seamlessly for modern ciphers and protocols, yet the requests with legacy encryption are being redirect to the weak encryption page. You can try out the setup as follows:

$> curl --tlsv1.2 --ciphers "AES128-SHA" --verbose
*   Trying
* Connected to ( port 443 (#0)
* ALPN, offering http/1.1
* Cipher selection: AES128-SHA
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / AES128-SHA
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject:
*  start date: Februar  2 18:55:24 2020 GMT
*  expire date: May  2 18:55:24 2020 GMT
*  subjectAltName: host "" matched cert's ""
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
> GET /cms/ HTTP/1.1
> Host:
> User-Agent: curl/7.68.0
> Accept: */*
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Date: Mon, 10 Februar 2020 15:57:17 GMT
< Server: Apache
< Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
< X-Frame-Options: sameorigin
< Referrer-Policy: same-origin
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< Location:
< Content-Length: 227
< Content-Type: text/html; charset=iso-8859-1

(If you have TLSv1.3 support, the call for a weaker cipher is ignored silently. You need to make sure you're using a lower TLS version. The same is true with TLSv1 and TLSv1.1. At least my curl is not supporting it at all.)

This call should redirect you to

Technically, it should be possible to do the same thing with a pure ModSecurity construct. However, the problem is that the SSL variables are not accessible by ModSecurity (and the directive SSLOptions +StdEnvVars does not help. The issue has been reported before).

I am not sure if you can do the same with NGINX too. NGINX is usually less flexible, but this is a case where it might be possible to pull this of on said platform just as well.

If you want to deploy a more complex error page - one meeting the corporate branding with the inclusion of logo and separate CSS files, then you will have to extend the conditional statements or - better still - embed CSS and images right into the weak-encryption.html page.

Good luck!

If you liked this blog post, then there is more on this site or why don't you follow me on twitter or subscribe to my ModSecurity / OWASP Core Rule Set newsletter.

I am also covering this and other advanced configuration as part of my teaching. Here is a link to me next public courses.

Christian Folini / @ChrFolini on Twitter