Preventing drive-bys with client certs


  • Tue 01 November 2016
  • misc

As I mentioned in a previous post, it's easy to gen up X.509 certificates running your own private certificate authority (CA). But what good will that do you? Those certs aren't trusted by any browsers, so what's the point?

The answer is that trust works in more than one direction. You can trust a web site because it presents a certificate that is signed by a certificate authority which your browser trusts. You can also have a web site (or email server, or whatever) trust a client if it presents a certificate which is signed by a certificate authority that your server trusts. This is how OpenVPN works.

Browsers come with a prepopulated trust store. In Mozilla's case, at this writing, 176 CA certs. In iOS 10, between "trust", "prompt for trust", and "block always", there are 203 CA certs in the trust store (as of this writing). Android apparently varies by vendor, and I don't have a Windows machine handy to test from, but you'll find that the major root certificate stores (Apple, Microsoft, Mozilla, and Android) substantially overlap in who they trust.

That's a good thing for browser certs and interoperability, but of no value whatsoever in terms of trusting clients. Indeed, running your own local certificate authority is actually a benefit here - you wouldn't want to misconfigure and accidentally trust everyone with, say, a WoSign or StartCom certificate, intermediate certs that are good for signing children are expensive, and support for pinning trust to an intermediate CA is variable (NGiNX has been known to have issues).

So let me lay out a scenario here. Let's say you're running Drupal as a CMS with Drupal Static to generate a static version of your site. That's the part of healthcare.gov that stayed up during the original Affordable Care Act goat rope, so it's a good choice... but at the same time you're keenly aware that on the management side you're kind of hanging out in the wind for every vuln that might come along and you're starting to feel as if you're running Wordpress considering how often you're running updates.

Here's a variation on the same scenario... you're running Jira or Confluence and between the fact that you've got a whole lot of proprietary data in there and the fact that it's all written in Java, your skin crawls when you think about security.

In both cases, you'd really like to have your site open to the Internet for ease of use by your geographically diverse, traveling team. You don't want to force them to use a VPN, since that's just a pain in the butt. At the same time, you don't want to depend on just a username and password to log in, which offers no protection against logged-out vulnerabilities anyway.

You probably don't care about single-signon and tying the username to the cert, or anything like that. You just want a piece of armor around the whole existing affair to keep anyone who isn't part of the club from so much as talking to your application.

It turns out that this is pretty easy to do, and only two lines to add to your nginx.conf file. OK, it's three if you want to use a CRL. That's optional, but highly highly recommended. You do use NGiNX right? Friends don't let friends use Apache. For things that have a built-in web server or run on Tomcat or whatever, you can stick NGiNX in front of it and protect the back end and this trick works great too.

One caveat here - I'm going to show you how to do this with screen shots of a running instance of Nextcloud which we use around here for a bunch of stuff including caldav and carddav syncing. You might infer from my choice of demo material that client side certs work on iOS devices for caldav and carddav, but you'd be mistaken. I wish Apple would fix this; it would make me happy (not as happy as an escape key but I digress...).

The magic config bits are ssl_client_certificate, ssl_crl, and ssl_verify_client. I put them in the server { stanza in my nginx.conf right below the SSL certificate keys, like so:

{% raw %} ssl_certificate /etc/ssl/local/server.crt ; ssl_certificate_key /etc/ssl/local/server.key ;

    ssl_client_certificate cool-kids-ca.crt;
    ssl_crl cool-kids-crl.pem ;
    ssl_verify_client on;

{% endraw %}

Restart nginx, now try to visit the site. Voila, now Safari wants you to select a client certificate (which we haven't installed yet because we wanted to see what happened if we didn't have a valid one).

Safari demanding a client certificate

Clicking through, NGiNX nopes out on us. Note that this isn't the application throwing an error, it's NGiNX. The application never got touched, which was the whole point.

Clicked through, NGiNX is unhappy

Unfortunately now we've got a mess on our hands. We have to go into Keychain Access.app and get rid of the identity preferences for the web site, because Keychain doesn't know that we have a bad cert, it just knows that we selected a cert and that's what we should use from now on.

Identity preferences were saved, have to delete and restart Safari

Now we import our cert/key pair (concatenated file from the last blog entry, just drag and drop on Keychain Access.app. But there's one more thing to do - since the Mac does not know of our "Cool Kids CA" (and you probably don't want to add it as a trusted CA for browsers if you're only doing client side certs with it), the default trust settings won't work (this is normal behavior - Java does it too). You'll need to change the trust settings on this cert so we can use it for SSL. Do a "get info" on your client cert in your login keychain and the right thing to do will be apparent:

While we're at it, trust the cert (default is no trust because signed by unknown CA)

Restart Safari if you havn't since you got rid of the identity preferences (Safari seems to cache this internally). Go back to the site. Note that our client cert is there.

Now it will present this cert as an alternative

A couple of clicks later, we're off to the races.

Success!

Hopefully I've presented this in a form where it's repeatable but I'd love to get some feedback (like, why is my appleid certificate being offered, what's with the extra signing of stuff when I select the right certificate, tips for scripting the interaction with Keychain Access.app, etc). Will add on any information I get here.