HOWTO setup ssl client authentication with openssl, nginx, a backend of your choice and self signed certificates

step 1 - create the CA

A certification authority is required for signing client certificates, so the steps for creating one are these:

Now you might want to check what actually happened.

openssl x509 -in ca.crt -text

we just created the key (ca.key) and signed the certificate request: now we have a valid certificate (valid only for ourselves).

step 2 - create the webserver certificate

This step can be skipped if we have already a server certificate (a paid one from the trusted CAs).

In order to create our webserver certificates we have to follow these steps:

Again, we can check the generated certificate

openssl x509 -in example.com.crt -text

step 3 - first nginx.conf

The first attempt to see if the server certificate works:

[...]
http {
    [...]
    server {
        listen 443;
        server_name example.com;
        ssl on;
        ssl_certificate /path/of/my/example.com.crt;
        ssl_certificate_key /path/of/my/example.com.key;
    [...]
}
[...]
        

step 4 - create a client test certificate

Again the steps are key - request -sign, plus the pkcs12 export of the certificate, well known format by the web browsers:

step 5 - setup the client authentication in nginx

We have to edit the previous nginx.conf

[...]
http {
    [...]
    server {
        listen 443;
        server_name example.com;
        ssl on;
        ssl_certificate /path/of/my/example.com.crt;
        ssl_certificate_key /path/of/my/example.com.key;
        [...]
        ssl_client_certificate /path/of/my/ca.crt;
        ssl_verify_client optional;
        [...]
        #example to get some custom headers with ssl infos
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header X-SSL-client-serial $ssl_client_serial;
            proxy_set_header X-SSL-client-s-dn $ssl_client_s_dn;
            proxy_set_header X-SSL-client-i-dn $ssl_client_i_dn;
            proxy_set_header X-SSL-client-session-id $ssl_session_id;
            proxy_set_header X-SSL-client-verify $ssl_client_verify;
        }
    [...]
}
[...]
        

step 6 - create a test server

In the example configuration I chose the reverse-proxy, so I'll create a backend server to display that everithing is working well.

This task can be accomplished with almost every programming language and framework of your choice (.net, java, python, ruby...).

I'll create a node.js server just for the small effort that it require.

The server code (server.js) will simply output a copy of the request(without the request body):

require('http').createServer(function(req, res) {
    res.writeHead(200)
    res.write("<pre>")
    res.write(req.method + " " + req.url + " HTTP/" + req.httpVersion + "\n")
   for (var name in req.headers) {
        res.write(name + ": " + req.headers[name] + "\n")
    }
    res.end("</pre>")
}).listen(8080)
        

And launch it with:

node server.js

step 7 - test the setup

The first run, without client certificates, this is the output pointing at https://example.com:

GET / HTTP/1.0
x-ssl-client-session-id: [...very long unique session id...]
x-ssl-client-verify: NONE
host: 127.0.0.1:8080
connection: close
user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-language: en-us,en;q=0.5
accept-encoding: gzip, deflate
accept-charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
dnt: 1
        

Now I'll install the client's certificate. In firefox go to

Preferences > Advanced > View Certificates > "Your certificates" tab > Import
and pick the client.p12 file.

Now restart the browser (to get rid of the session id), and point again the browser to our website. This time firefox will ask which client certificate to use. Select our client's certificate and this is what should happen:

GET / HTTP/1.0
x-ssl-client-serial: 02
x-ssl-client-s-dn: /C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=client_certificate
x-ssl-client-i-dn: /C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=my_ca
x-ssl-client-session-id: [...very long unique session id...]
x-ssl-client-verify: SUCCESS
host: 127.0.0.1:8080
connection: close
user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-language: en-us,en;q=0.5
accept-encoding: gzip, deflate
accept-charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
dnt: 1
        

final - automate the generation

This is a small shell script that I wrote to automate the generation of client certificates (batch generation).

#!/bin/sh
SERIAL=$(cat demoCA/serial)
KEY=client-$SERIAL.key
openssl genrsa -out $KEY
SUBJECT="/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=client-$SERIAL"
CSR=client-$SERIAL.csr
openssl req -new -subj $SUBJECT -key $KEY -out $CSR -batch
CRT=client-$SERIAL.crt
openssl ca -in $CSR -cert ca.crt -keyfile ca.key -out $CRT -batch
P12=client-$SERIAL.p12
openssl pkcs12 -export -clcerts -in $CRT -inkey $KEY -out $P12 -password pass:
        

Troubleshooting

I've tested the steps with archlinux and encountered these small problems.

The path /etc/ssl has to be replaced with /etc/openssl or something else in some systems

openssl complains that /etc/ssl/index.txt can't be found

touch /etc/ssl/index.txt

openssl complains that /etc/ssl/serial can't be found

echo 01 | tee /etc/ssl/serial

openssl complains that the /etc/ssl/newcerts directory don't exists

mkdir /etc/ssl/newcerts

TXT_DB error number 2

We are signing two certificates with the same common name (CN). Every operation is recorded by openssl in its database, we can clear everything if it's a test run, or revoke the signatures if we have the old certificates (the suitable action if we are replacing an expired certificate).

To clean the mess instead, for our testing purpouses delete /etc/ssl/index.txt, /etc/ssl/serial and /etc/ssl/newcerts and start over with a fresh configuration.

links