« Previous Next »
29 October 2014

TL;DR

  • Using client certificate verification with nginx?
  • Don’t put it on your nginx default_server if you use SNI!
  • And, make sure you have that default_server without it!

In order to keep this article short, I assume the reader has sufficient knowledge about the following topics:

  • TLS Server Name Indication.
  • Configuring client certificate verification in Nginx.
  • The HTTP/1.1 Host header and how different server declarations, the explicit or implicit default_server and the server_name work together.

So what’s this all about?

While doing some testing with nginx and verification of client certificates some time ago, I ran into an interesting situation.

Consider the following nginx configuration example:

server {
    listen [::]:443 default_server ipv6only=on ssl;
    listen 0.0.0.0:443 default_server ssl;
    server_name _;
    ssl_certificate /etc/ssl/nginx/CA-default-server.crt;
    ssl_certificate_key /etc/ssl/nginx/CA-default-server.key;
    ssl_client_certificate /etc/ssl/nginx/CA-default.crt;
    ssl_verify_client on;
    location / { deny all; }
}
server {
    listen [::]:443;
    listen 0.0.0.0:443;
    server_name CA1-server;
    ssl_certificate /etc/ssl/nginx/CA1-server.crt;
    ssl_certificate_key /etc/ssl/nginx/CA1-server.key;
    ssl_client_certificate /etc/ssl/nginx/CA1.crt;
    ssl_verify_client on;
    location / { return 200; }
}

I configured the default server block to require connected clients to present a certificate signed by CA-default before they can do a request on this default server block. Besides that, there’s a specific server block for hostname CA1-server, which requires a client certificate to be presented which is signed by CA1.

Now, if I issue the following request…

  • no SNI specified
  • client sends certificate signed by CA-default…
  • …and then does a request on the other server block, CA1-server..

    $ openssl s_client -connect 127.0.0.1:443 -cert CA-default-client.crt -key CA-default-client.key […] GET / HTTP/1.1 Host: CA1-server

    HTTP/1.1 200 OK Server: nginx Date: Sat, 04 Oct 2014 20:47:36 GMT Content-Type: application/octet-stream Content-Length: 0 Connection: keep-alive

    QUIT DONE

…the request is handled, and I get the 200 answer.

Whoa! That’s not good, because I never presented a valid certificate, signed by CA1, to the server. Why is the CA1-server not requiring this?

Is this a bug? Or is it a feature? Let’s try it the other way round, by connecting using SNI to the CA1-server, and then requesting content from the default server:

$ openssl s_client -connect 127.0.0.1:443 -servername CA1-server -cert CA1-client.crt -key CA1-client.key
GET / HTTP/1.1
Host: foo
HTTP/1.1 400 Bad Request
Server: nginx
Date: Sat, 04 Oct 2014 20:44:49 GMT
Content-Type: text/html
Content-Length: 166
Connection: close

<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
closed

Aha! So, the actual issue is:

When the default server block has client certificate verification enabled, it allows the connected client to do requests on other server configuration blocks, while ignoring the client certificate verification options of that other server block.

If TLS SNI is not used when establishing the connection, the ssl options of the default server block are used. As soon as the connection is established, I’m free to issue whatever HTTP request I want.

By inserting a Host: header in my request that targets the CA1-server server block, nginx will jump to that server block and try to handle my request. The ssl_certificate, ssl_certificate_key and ssl_client_certificate from the CA1-server will not be considered at all, because I did not use SNI when establishing the connection. Anyway, nginx will still consider the ssl_verify_client on option, and because my presented client certificate could be verified against the CA specified in the non-SNI default_server, the ssl_verify_client check is successful and I get my HTTP 200 OK result.

I asked the nginx developers about this, and the answer was clear: “While the behaviour observed may be somewhat counter-intuitive and not well documented, it’s actually the expected behaviour.”


I don’t have a default_server, am I safe?

No, because if you do not explicitely define a default_server, the first server block encountered in your configuration will become default_server automatically. If it has client certificate verification enabled, the same trick can be used.


Conclusion…

Here’s a mix of the response I got from the nginx team and my own opinion about how to handle this:

When enabling ssl_verify_client on a server block that is not the default server block, connecting clients who do not support SNI will still use the SSL options specified in the default server block to establish connections. This includes ssl_ciphers, ssl_certificate, and ssl_client_certificate as well. Other options, like ssl_verify_client, ssl_verify_depth, and ssl_prefer_server_ciphers will still be used as specified on the server block that will be chosen to handle the actual request, based on the HTTP Host header in the request.

When using client certificate verification in combination with SNI, it’s recommended to always specify an explicit default_server configuration that sets ssl_client_verify to off. This will ensure that non-SNI connections will not be allowed to request resources from server blocks with ssl_client_verify set to on.

Alternatively, don’t rely on SSL certificate authentication to also do authorization, and introduce a separate authorization step. That is, check $ssl_client_s_dn and/or $ssl_client_i_dn variables as provided by the SSL module to see if access should be granted to a particular resource.

What we do at Mendix, is always using a simple default_server block, which we call a ‘catch-all’, that does not have certificate verification enabled and just shows a 403 Forbidden or some fancy html error page for everything, and then more server blocks are added after that (optionally requiring a client certificate) with their own specific url. This way also all random requests from the internet connecting to the IP address or fqdn which belongs to that address who are searching for phpmyadmin and everything always end up at the simple default server which denies them. :)

And yes, that means that when you want to use client certificate verification on your Mendix application, you need to use client software that can use SNI. Bye bye Windows XP and Java 6!

Posted by Hans van Kranenburg

Hans works as system/network engineer and software developer for Mendix since the founding of the company (or even before that). He likes building computer networks using linux, using IPv6 and routing protocols.

blog comments powered by Disqus