Using Let's Encrypt with Certificate Based Authentication

For one of my sites I wanted to use TLS client authentication. It’s easy enough to setup in Apache:

<VirtualHost *:80>
  …
  RewriteEngine On
  RewriteRule (.*) https://%{SERVER_NAME}$1 [R=301,L]
</VirtualHost>

<VirtualHost *:443>
  …
  SSLEngine on
  SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
  SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
  SSLVerifyClient require
  SSLVerifyDepth 1
  SSLCACertificateFile /srv/apache/data/root.crt
  SSLRequire (%{SSL_CLIENT_S_DN_CN} == "Me") \
          || (%{SSL_CLIENT_S_DN_CN} == "Myself") \
          || (%{SSL_CLIENT_S_DN_CN} == "Irene")
  SSLUserName SSL_CLIENT_S_DN_CN
</VirtualHost>

Illustration

And this worked just fine for 90 days or so. More precisely, it worked until CertBot had to update my Let’s Encrypt certificate.

Guess what? Let’s Encrypt doesn’t have knowledge of my client certificate and thus handshake fails. Error message is not really helpful as “tls: unexpected message” doesn’t really point you to the correct path. Fortunately, I actually remembered my certificate shenanigans and thus was able to debug it quite quickly. Issue verification was as easy as dropping certificate requirements made my renewal work again.

However, dropping certificates every month or two would not work for me. I wanted something that would work the same as automatic renewal for other Let’s Encrypt certificates. And no, you cannot set .well-known directory to use different validation. With TLS 1.3, you cannot change client requirements once connection is established. You’ll just get “Cannot perform Post-Handshake Authentication” error.

But, you know where you can play with locations to your heart’s content? In HTTP section. Instead of just redirecting to HTTPS, you want to carve small hole for CertBot verification.

<VirtualHost *:80>
  …
  RewriteEngine On
  RewriteRule (.*) https://%{SERVER_NAME}$1 [R=301,L]
  <Location "/.well-known/">
    RewriteEngine Off
  </Location>
</VirtualHost>

Now Let’s Encrypt verifies renewal requests using HTTP which is not really a security issue as verification file is completely random and generated anew each time.