When it comes to authentication on the web we’re pretty much all familiar with the password – king of the hill, infamous for both its flaws as well as its ubiquity. Outside of the password, you often run into authentication schemes such as token sharing (e.g., OAuth or RSA tokens).
One other method you’ll very infrequently see is the lowly client certificate form of authentication. Client certs are related to the much more popular server certificates and exist in the same TLS handshake. In other words, the same initial connection from your web browser that asks the server if it has an SSL certificate, also involves the server asking the browser if it has an SSL certificate. The former’s adoption has been significant over the past decade due to its usage in HTTPS. Client certificates are only useful for authentication, so by comparison their adoption has been more limited than server certificates.
Despite its lack of popularity and a few cumbersome aspects, the client cert is compatible with modern desktop browsers and web servers. For some use cases, it makes sense to forgo the absolute convenience of the password for the absolute security of SSL certificates.
Putting It All Together
There are a few ugly defaults we need to cover, but I don’t want to discourage you by getting too technical early on. Instead, let’s work backward and take a look at how this should all come together.
Pictured above you can see that Firefox’s Certificate Manager has the ability to store personal certificates. When utilizing client cert authentication, the user’s browser would have a certificate from a trusted Certificate Authority. Pictured, you can see that I have two, one from Comodo for my email, and one self-sign certificate for auth. We’ll get into the self-signed bit later, but this does not represent the same risk as self-signed server certificates.
Assuming you have a client certificate in your browser (you don’t yet, but don’t worry we’ll circle back to that), your browser will automaticly send it to the server when performing the same TLS handshake used to negotiate HTTPS. This is assuming that the server actually asks for a client cert.
The above Nginx configuration is an excerpt from this website’s configuration. Lines 2-11 & 17 are pretty much your basic HTTPS configurations. I include those for completeness since negoating HTTPS is a prerequisite for client certificate authentication to work; but, I honestly hope you’re already doing that.
Lines 13, 14, and 15 are the real meat and potatoes of the server configuration;
ssl_verify_client – this is set to optional, meaning that we ask the browser for a client certificate if it has one, and validate it if supplied, but Nginx will not fail the TLS handshake if the browser does not provide one.
ssl_client_certificate – this configuration tells Nginx which Certificate Authorities to trust. Much like how web browsers maintain a list of trusted CAs, this allows your server to have a similar list. Nginx will list these CAs by name during the handshake.
ssl_verify_depth – finally, this setting lets Nginx know how many intermediate certificates to allow for. Client certificates are rarely signed directly by the Certificate Authorit’s root certificate; therefore, setting this to 2 accommodates 1 intermediate signer in addition to the root certificate, which is pretty typical.
The above three configuration elements are all that is required to enable client certificates in Nginx. There are two remaining considering; how do we generate these certificates, and how do we impose a meaningful form of authentication? Let’s discuss the latter first.
It’s one thing to ask the browser to supply a certificate, it’s something else to actually authenticate a user. In the preceding section, we set ssl_verify_client to optional. The rationale for this is that most websites have a mixture of public and private content. You would not want to preclude users from accessing public content, simply because they lack the necessary certificate for private portions. Even entirely private applications will want to expose some type of custom page for those not current authenticated – e.g. a login or registration page. For all of these reasons, the recommendation is to leave ssl_verify_client to optional and handle any additional logic in either the application or in Nginx location rules.
Nginx provides two variables that are useful for asserting authentication – ssl_client_verify and ssl_client_raw_cert.
The ssl_client_verify variable, in its most basic form, equals SUCCESS when a client certificate has been presented and matches the server’s trusted list of CAs (as set with ssl_client_certificate). You can refer to the Nginx documentation for other values it contains, but for most use cases it is sufficient to simply test equality or inequality to SUCCESS. For example;
The above excerpt is taken from this website’s Nginx configuration. In this example, you can see that ssl_client_verify is compared to SUCCESS for anyone attempting to access the WordPress login page of this site. Supplying a valid client certificate will pass the request to WordPress for regular password-based authentication – so no deviation from standard setup there. However, for those browsers without a valid client certificate, their connection will be immediately terminated with a 404 – Access Denied error.
In this use case, client certificate authentication is only providing an extra layer of security. A much need one, considering the rampant number of automated hacking attempts performed against WordPress sites. This security measure also assumes that simple validation of the certificate is enough to filter out the riff-raff. This works because Nginx is configured to only trust a set list of certificate authorities (see the ssl_client_certificate discussion above) – in my case my own CA.
For more fine-grained control, the ssl_client_raw_cert variable allows for inspection of the client certificate itself. This variable is PEM encoded, and therefore requires additional processing to make it useful. For this reason, it is best to pass this variable on to the upstream server for application-level inspection. In other words, we will stuff this variable into an HTTP header and allow Ruby, Node.js, .NET, Java, etc. to process the PEM formatted certificate in the application layer, and either allow or reject the request.
The above snippet illustrates how such a variable can be forward to the upstream server. By convention HTTP header starting with “X” are custom, and there one-time free to choose whichever name we want, just so long as Nginx and the upstream application agree on the naming convention.
Above is an example of what the contents of that header will look like – a PEM encoded certificate.
It is entirely up to the client application on how to handle things from this point onward. For comparison, however, this is conceptually not that much different than handling password-based authentication. The application layer should have logic to validate the supplied username and password, and to respond accordingly. Similarly, an application making use of client certificate authentication should have logic to process the PEM encoded certificate, extract whichever fields it deems relevant (most likely the Common Name) and respond accordingly. The application does not need to validate the certificate, as the validate was handle handled by Nginx before forwarding the request.
The Ugly Details
Keys and certificates – say those words to most developers and they’ll cringe. Rightfully so, there is little in the way of user friendless – truly a product of function over form. Love them, or hate them, they’re a necessity in our modern world. Having stood the pressures of decades of cryptoanalysis, they’re number one priority is to secure digital assist, not necessarily making developer’s lives easier. It is for these reasons that I decided to cover this topic last.
If you really want to skip this section, you are always welcome to purchase a commercial certificate. Venders such as Comodo sell client authentication tickets for about the price of a dinner for two – though this is a yearly fee. Other than cost, the downsides to a commercial certificate are that simple validation with ssl_client_verify, like we saw above, is not as practical. In that example, I showed how this very blog uses the validity of the certificate alone as a gatekeeper. This works for my blog because it is validating against my own Certificate Authority. Since I don’t generate certs for everyone, I know if someone has an authentic cert they’ll highly likely me.
Unlike server certificates, there are little downsides to self-signed client certificates. The dangers around self-signed server certificates stem from the client trust aspect. Trusting any certificate boils down to trusting one or many Certificate Authorities to securely and properly issue certificates. Browsers trust Certificate Authorities based on a well-vetted curated list of CAs bundled into each browser. When confronted with an otherwise valid looking certificate, browsers will rejected the connection if not from a trusted CA. Users can override this behavior, but such behavior is discouraged due to the dangers of a user inadvertently trusting the wrong cert. The likelihood of a man-in-the-middle attack is too high, and most websites do not use self-signed certs. Therein lies the dangers.
When properly distributed, self-signed certificates can be just as safe as commercial certificates – but that is a BIG if for server certs. For client certs the risks are virtually elimenated. Browsers do not care who issues their client certificates since they retain both the public and private key. The server, presumably operated by the same person or organization that is self-signing certs can inherently trust themselves – I hope.
Enough stalling on my part, let’s look at how we generate these certificates.
The first step is that we must become a Certificate Authority. Using OpenSSL this is relatively easy.
First, check the dir setting in the [ ca_default ] section of the OpenSSL configuration – you will typically find this in /etc/ssl. The dir setting should point to ca, making the full path /etc/ssl/ca – that is the path these examples will use. If your path does not match, either update the OpenSSL config or the following scripts to match one another.
With that verified, it is time to create our Certificate Authority.
The above script takes care of all of the heavy lifting. You will be prompted for a password for the CA’s certificate – please remember this, it is not something that you will want to forget. With this one time setup complete, we are now ready to create one or many client certificates. This too can be automated into a simple script.
The username of the client should be supplied as the first argument when calling this script. Since we’re our own CA, this username can be anything we want – even an email address. The only requirement is that it’s unique within our own CA.
When the create-user script completes you will have a password protect PKCS #12 formatted client certificate ready for importing into your browser. You’re done – go ahead and import it. Nginx location paths protected in earlier sections should now be accessible.
In Wrap Up
Client certificate authentication, while not practical for all scenarios, is a valuable tool to have at your disposal. With support built right into modern desktop browsers and Nginx, the setup can be completed in a few minutes but yet provide protection far superior to regular passwords. By far the biggest downsides to this technique is its lack of support in mobile browsers and the hassles associated with the initial distribution of the client certificate. Do not expect this to replace passwords, but is an ideal option for added security on private admin pages for small companies or personal websites.