Adding Client Certificate Authentication to Existing https Setup

I use haproxy in a SSL termination config, where depending on the URL the traffic is directed to different backends.

I auto generate a SSL certificate using Let’s Encrypt. Clients are just Web browsers and I currently authenticate using usernames and passwords for each backend. I can either enable or disable the authentication. I cannot modify the backends to accept client certificates.

I would like to use client certificates for authentication on the front end and therefore remove the need for username and passwords on the backend. According to this https://arcweb.co/securing-websites-nginx-and-client-side-certificate-authentication-linux/ for nginx some additional lines need to be added to enable client authentication, and once authenticated, the rest of the traffic is encrypted.

How can I achieve the same thing with haproxy?

I’m aware that in some instances certificates can be combined (eg TLS with Client Authentication) but I’m not sure if this is required for haproxy nor how to do it.

On the front end I have the following line related to ssl:

 ` bind 199.99.99.99:443 ssl crt /etc/haproxy/certs/hostname-dh.pem`

What config changes do I need to make to add client authentication?

I wrote a Blog that might act as a good starting point for you: https://www.loadbalancer.org/blog/client-certificate-authentication-with-haproxy/

Basically, you need to add a CA and possibly a CRL if you also want to be able to easily revoke certificates.

So minimum requirement would be:

bind 199.99.99.99:443 ssl crt /etc/haproxy/certs/hostname-dh.pem /etc/haproxy/cert/ca.crt verify required

Thanks it’s what I’m looking for.

A quick follow up I have is that I use ACLs based on path_beg to determine which backend to direct them to. How do check if a client certificate is valid and path_beg is met?

In your example you have the opposite, all paths allowed except secure. I want to (for the sake of simplicity) state each path individually. For example I have the following:

acl url_gui path_beg /gui
use_backend www-sync if url_gui

acl url_sick path_beg /sick
use_backend www-sick if url_sick

I want to modify it so that backend www-sick is only used of the path begins with /sick and client certificate is valid.

What I found in my testing is that when using “verify optional” to allow both those with and those without the client cert access I had to verify two things to be sure it was valid.

  1. Was the client cert used?
    ssl_c_used 1
  2. Did it verify?
    ssl_c_verify 0

I couldn’t just use “Did it verify” as I found “ssl_c_verify” returned “0” even when a client certificate wasn’t present… However, if it was used so “ssl_c_used” “1” then it will have been checked against the CA so you can now trust “ssl_c_verify”.

There may be a better way to do this that I missed but something like this should do it:

acl cert_present ssl_c_used 1
acl cert_verified ssl_c_verify 0
acl url_sick path_beg /sick
use_backend www-sick if url_sick cert_present cert_verified

Sorry, I haven’t actually tested it but AND is implicit so that should result in, send to backend “www-sick” if “url_sick” and “cert_present” and “cert_verified”.

I’ve been trying this out and I suspect the overall configuration is correct (with regards path etc), however it’s not working and I wonder if it’s something trivial I’m doing.

I followed this guide to generate a root, intermediate and device certificate:
https://jamielinux.com/docs/openssl-certificate-authority/sign-server-and-client-certificates.html

I left the hostname-dh.pem from above alone. For the ca.crt I concatenated the root, intermediate and device certificates (and called it bundle.crt), is that correct?

For the device, I ran the following command:

openssl pkcs12 -export -out device.pfx -in key device.private.key -in bundle.crt 

I installed this on Chrome in Android and I get a pop-up asking me to select the certificate.

Any ideas?

The CA file, “ca.crt” in your example should only be the root and intermediate certs I think… No need to include the device cert as this will have already been signed by the CA or any intermediate CA.

Can you see the output of ssl_c_verify in your testing? The number presented in this fetch represents the status code of the client SSL handshake so acts as a good troubleshooting method.

Common codes I ran into in my testing are:

10 = Client Cert Expired.
23 = Client Cert Revoked
21 = Cert not authenticated, not correctly signed by this CA.
26 = Wrong Cert usage, invalid purpose.

FYI you can find the complete list of openssl error codes here:

https://www.openssl.org/docs/man1.0.2/apps/verify.html

Thanks guys. It’s working now. Here is what I think went wrong:

1st attempt, I didn’t follow the instructions carefully when generating the device certificate from the guide I was following, specifically the line regarding server_cert instead of usr_cert. I also tried changing the ca-file from having just root and intermediate certs to root, intermediate and device.

2nd attempt I followed the instructions carefully but was tying to be too clever by using ECDSA to generate the private keys. As I was testing at the time with my Android (7.1.2) phone, I couldn’t get the pkcs12 to install.

3rd attempt, I followed the cert instructions and used RSA. I’m not sure if it made a difference but I didn’t put a password on the device private key (which might be removed during pfx export?), but otherwise did the same thing as above and it all works on my phone and laptop.

bind 199.99.99.99:443 ssl crt /etc/haproxy/certs/hostname-dh.pem ca-file /etc/haproxy/cert/root-int.ca.pem verify optional

acl cert_present ssl_c_used 1
acl cert_verified ssl_c_verify 0
acl url_sick path_beg /sick
use_backend www-sick if url_sick cert present cert_verified