Client Side Certificate Auth in Nginx

Why Client-Side Certificate Authentication? Why nginx?

I sometimes peruse the ReST questions of stackoverflow.com. Many times I see questions about authentication. There are many options (Basic HTTP Auth, Digest HTTP Auth, OAuth, OAuth Wrap, etc.) however when security is of importance, I like to recommend client side certificates. This is the route our team at ShowClix chose when implementing our API.

When first implementing the API Authentication, we were using Apache for our ReST API Servers. It took some serious google-fu and tinkering to get Apache cooperating with the client-side certs and passing that info into our PHP App layer. I remember it being a semi-painful process.

Lately, I've become a huge fan of nginx. Its clean, familiar config syntax and speed make it a great alternative for Apache in many cases. Its reverse proxy capabilities are quite nice as well. So, I thought I'd give client-side cert authentication a shot in nginx. Whereas a quick search for "Client Side Certs in Apache" yielded a few relevant results, a similar search for nginx yielded no results, so I figured I'd share here.

I ran this on a small 256MB Rackspace cloudserver instance running Arch Linux, nginx 0.7.65, PHP 5.3.2 and PHP FPM.

Creating and Signing Your Certs

This is SSL, so you'll need an cert-key pair for you/the server, the api users/the client and a CA pair. You will be the CA in this case (usually a role played by VeriSign, thawte, GoDaddy, etc.), signing your client's certs. There are plenty of tutorials out there on creating and signing certificates, so I'll leave the details on this to someone else and just quickly show a sample here to give a complete tutorial. NOTE: This is just a quick sample of creating certs and not intended for production.

# Create the CA Key and Certificate for signing Client Certs
openssl genrsa -des3 -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt

# Create the Server Key, CSR, and Certificate
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr

# We're self signing our own server cert here.  This is a no-no in production.
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

# Create the Client Key and CSR
openssl genrsa -des3 -out client.key 1024
openssl req -new -key client.key -out client.csr

# Sign the client certificate with our CA cert.  Unlike signing our own server cert, this is what we want to do.
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

Configuring nginx

server {
    listen        443;
    ssl on;
    server_name example.com;

    ssl_certificate      /etc/nginx/certs/server.crt;
    ssl_certificate_key  /etc/nginx/certs/server.key;
    ssl_client_certificate /etc/nginx/certs/ca.crt;
    ssl_verify_client optional;

    location / {
        root           /var/www/example.com/html;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_param  SCRIPT_FILENAME /var/www/example.com/lib/Request.class.php;
        fastcgi_param  VERIFIED $ssl_client_verify;
        fastcgi_param  DN $ssl_client_s_dn;
        include        fastcgi_params;
    }
}

The main things to note here are...

  • We specify our the server's certificate (server.crt) and private key (server.key)
  • We specify the CA cert that we used to sign our client certificates (ca.crt)
  • We set the ssl_verify_client to optional. This tells nginx to attempt to verify to SSL certificate if provided. My API allows both authenticated and unauthenticated requests, however if you only want to allow authenticated requests, you can go ahead and set this value to on.
  • Lastly, you'll notice that I add a location directive that routes all the requests to a single PHP script. You can handle this differently (and technically don't even need to use PHP as there are other fast cgi options)

Passing to PHP

There are several options for running PHP from nginx. I chose to use PHP FPM, however these steps should also work for any of the fast cgi options in theory. You'll notice I added a few additional fastcgi_params to the usual fastcgi_params.

First, we pass in the $ssl_client_verify variable as the VERIFIED parameter. This is useful when we are allowing authenticated and unauthenticated requests. When the client certificate was able to be verified against our CA cert, this will have the value of SUCCESS. Otherwise, the value will be NONE.

Second, you'll notice we pass the $ssl_client_s_dn variable to the DN parameter. This will provide "the line subject DN of client certificate for established SSL-connection". The Common Name part of this certificate may be of most interest for you. Here is an example value for DN...

/C=US/ST=Florida/L=Orlando/O=CLIENT NAME/CN=CLIENT NAME

Nginx also provides the option to pass in the entire client certificate via $ssl_client_cert or $ssl_client_cert_raw. For more details on the SSL options available to you in nginx, checkout the Nginx Http SSL Module Wiki.

Consuming the ReST Service

So, we've created our certs, signed our client certs, installed nginx and PHP, and setup nginx verify the certs and finally pass along client cert details. Now we are ready to consume our ReSTful service.

There are lots of tools out there for consuming true HTTP based ReSTful services. Some of them are prettier than others, but I prefer the good old cli version of cURL.

NOTE: This is run from your client computer. Make certain that you have scpd your client.key and client.crt files from your server onto your client machine that is making the requests. You'll also be prompted for the pass phrase you used when you first created the client cert. There are ways to remove the need for pass phrases. Also, you don't need the verbose flag (-v) or silent flags (-s). We're using the -k flag here because we used a self-signed cert for the server.

curl -v -s -k --key client.key --cert client.crt https://example.com

Source code is also available on git hub.

79148 views and 9 responses

  • Jan 26 2011, 4:28 AM
    Riccardo Rodella liked this post.
  • Apr 2 2011, 6:32 PM
    Carter Allen liked this post.
  • Apr 2 2011, 6:33 PM
    Carter Allen responded:
    Thanks for this incredibly helpful article!
  • May 18 2011, 10:10 AM
    kchmiel (Twitter) responded:
    Thank you. Great article. I'm having a problem though - after following your directions exactly (and trying it twice) I'm still getting an error (below) when I run the curl test against my nginx server. Any idea how I can debug or what could be wrong? Thanks, Kevin




    <center>

    400 Bad Request

    </center>
    <center>The SSL certificate error</center>

    <center>nginx</center>

  • Jul 10 2011, 10:48 AM
    X responded:
    my 2c... I also got the 400 error, and almost pulled what little hair i have left out trying to fix it.

    quick fix:
    curl -v -s -k --key ca.key --cert ca.crt https://example.com

    it looks like nginx wants certs that are valid for the clients and not certs that have been signed by someone.
    You can 'cat cert1.crt cert1.crt > clients.crt' then point to it ssl_client_certificate /etc/nginx/certs/clients.crt;

    I also added
    error_log /var/log/nginx/debug.log debug;
    to see what ssl errors came up

    Can someone else please comment on this little issue please, im sure my little workaround is not how to fix it
    When creating clien certs doest openssl want -extensions client_cert somewhere ?

  • Oct 31 2011, 10:50 AM
    alexeybobok (Twitter) responded:
    You have to export CAROOT cert into "ssl_client_certificate". That cert which was used to sign user cert. Tested :)
  • Apr 30 2012, 2:47 PM
    PlatypusBrother (Twitter) responded:
    You shouldn't use same serial number for the different certificates of one CA. For your example, Google Chrome ends up with strange "Unknown error", but Opera works just fine. It takes some time time to figure out what happens, but finally I have a found a great bug report: http://code.google.com/p/chromium/issues/detail?id=81960.
    btw thank you for the useful post.
  • Nov 2 2012, 9:47 AM
    rfkrocktk responded:
    I understand why we don't want to sign our own server certificates for security reasons, but how then does that work with client certificates? In production, would I still generate a CA and sign client certificates with it? If my server certificate was, in fact, signed with a proper CA, would I then generate my own CA and sign client keys with it?

    Also, is it possible to revoke certain keys, or would I have to generate a whole new CA and send out new keys to all users?

  • Nov 26 2012, 9:15 AM
    rynop responded:
    Thanks for the post. Very helpful.

    @rfkrocktk you don't have to generate a new CA. Piggbacking on this post, I have made a post of my own that goes into detail on this as well as I made some scripts to manage most of the process: http://rynop.com/howto-client-side-certificate-auth-with-nginx