indexpost archiveatom feed syndication feed icon

Client Certificate Authentication with HAProxy

2024-03-03

After configuring HAProxy on this server I've been trying out a few things that have been in the back of my mind for some time. Here I document configuring a client certificate to mutually authenticate a web browser to a subdomain and limit access based on its presence.

This was motivated by the experience of trying to guard access to a web application with just basic authentication. Client TLS certificates are such an obvious improvement in the security of the system that I wanted to understand what downsides I might be misunderstanding. In order to test it out I decided to try exposing the Cockpit instance on this server.

The problem I've had with running Cockpit was the idea of relying on the current user-password system on the server in an internet-facing environment. One of the first thing I did when I configured this server was to disable password authentication over SSH and this is basically an identical level of access. Instead I wanted the same kinds of guarantees I get with an SSH key - so I'll use an HTTPS client certificate.

the friendly manual

This was all nicely documented by HAProxy. There are a number of other scenarios for certificate generation in the Redhat manual; I'm capturing it here for my own notes.

I am just going to self-sign a certificate because I don't care about the longevity of this experiment (also why days is 30!):

openssl req -newkey rsa:2048 -nodes -x509 -days 30 -keyout privateCA.pem -out cacert.crt

Generate a certificate signing request for my browser (user):

openssl req -newkey rsa:2048 -nodes -subj "/CN=nolan/O=noprescoOrg" -keyout clientKey.pem -out client.csr

Create a certificate extension configuration file:

$ cat >certificate-extensions.conf <<EOF
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF

And then generate the certificate:

openssl x509 -req -in client.csr -out client.crt -CA cacert.crt -CAkey privateCA.pem -CAcreateserial -days 30 -extfile certificate-extensions.conf

On the one hand, I remember being entirely nonplussed the first time I had to do the above sort of thing with openssl. On the other hand, I've done it enough times that I really appreciate that there is nothing very interesting about the process; apart from the certificate extension selection this is nearly indistinguishable from creating a self-signed certificate for a web server. This makes sense but I had expected more steps to create a client certificate.

The last piece necessary in my case was to export the certificate in a format that Firefox can use, which meant converting to pkcs12:

openssl pkcs12 -export -out cert.pkcs12 -inkey clientKey.pem -in client.crt

With that done it is possible to copy the client certificate to my laptop and add it to Firefox from the preferences settings menu using the "Certificates → View Certificates → Import" dialog.

Configuring HAProxy

One thing that I hadn't really thought about was that TLS negotiation happens before application protocol layer handling such as HTTP so there's no way to configure a route that requires a client side TLS certificate. There is the possibility of making it optional, which I chose to do but I was reluctant to try it on the top-level domain in my case. Instead I carved out a temporary subdomain and accepted that I will receive warnings in the browser because it isn't listed in the certificate's Subject Alternative Names. My self-signed CA certificate is located at /etc/ssl/auth-test/cacert.crt so the configuration necessary to turn on certificate auth for cockpit.nprescott.com is then:

frontend https-in
    bind :::443 ssl crt /etc/ssl/nprescott.com/nprescott.com.pem alpn h2,http/1.1 verify optional ca-file /etc/ssl/auth-test/cacert.crt
    acl is_cockpit hdr_dom(host) -i cockpit.nprescott.com
    http-request deny if is_cockpit !{ ssl_c_used }
    use_backend be_cockpit if is_cockpit
    default_backend lighttpd-server

backend be_cockpit
    server c1 localhost:9090

Using It

Cockpit is already running on the server at localhost:9090 (and in reality it is systemd socket activated so it isn't really running, more like waiting to run). A quick reload of HAProxy and access attempts to the subdomain trigger my browser to pop-up a dialog telling me the domain has requested I identify myself with a certificate along with a selection of those certificates matching this domain. It is nice that there is an option to "Remember this decision" but I will say the browser interface leaves something to be desired in terms of user experience.

It seems like there is some work being done in the browser-space to improve the usability of client certificates. My impression is that this is probably most useful for things like hardware security tokens, which I haven't got to make an assessment of. The current "scary looking dialogs" are probably sufficient to deter most people from using this sort of thing. On the side of administering certificates the process of creating a single certificate for a single user for a single application is easy enough but I think there is not insignificant overhead to doing real certificates management. My impression is that the real thing involves something like FreeIPA or Dogtag PKI (I can't immediately tell!). I will admit I feel a sense of fatigue just looking at the number of acronyms and jargon surrounding PKI so I think I'll leave this here for now — the simple case is simple and then thing seem to rapidly spiral out to require a lot of supporting infrastructure.

I think there may be some interesting potential for access controls built on top of what I've found here, leveraging HAProxy in ways I've investigated before but I will either need to spend more time digging into certificate management or admit I don't know how it'd work in practice. I'm presently more interested in application-level uses but the breadth of PKI management leaves me a little flummoxed.