43
votes

I am trying to enable client certificate authentication in nginx where the certificates have been signed by an intermediate CA. I am able to get this working fine when using a certificate signed by a self-signed root CA; however, this does not work when the signing CA is an intermediate CA.

My simple server section looks like this:

server {
    listen       443;
    server_name  _;

    ssl                  on;
    ssl_certificate      cert.pem;
    ssl_certificate_key  cert.key;

    ssl_session_timeout  5m;

    ssl_protocols  SSLv2 SSLv3 TLSv1;
    ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_prefer_server_ciphers   on;

    ssl_client_certificate ca.pem;
    ssl_verify_client on;
    ssl_verify_depth 1;

    location / {
        root   html;
        index  index.html index.htm;
    }
}

For the contents of ca.pem, I have tried using only the intermediate CA and also concatenating the intermediate CA cert and the root CA cert, i.e. something like:

cp intermediate.crt ca.pem
cat root.crt >> ca.pem

I have also validated that the certificate is valid from openssl's perspective when using that same CA chain:

openssl verify -CAfile /etc/nginx/ca.pem certs/client.crt 
certs/client.crt: OK

I have experimented with setting ssl_verify_depth explicitly to 1 (as above) and then even 0 (not sure what that number means exactly), but still get same error.

The error I get in all variants of the intermed CA is "400 Bad Request" and more specifically "The SSL certificate error" (not sure what that means exactly).

Maybe nginx just doesn't support cert chains for intermediate certs? Any help greatly appreciated!

8

8 Answers

52
votes

Edit: I had also this "problem", solution and explanation is at the bottom of the text.

It seemed like nginx doesn't support intermediate certificates. My certs self created: (RootCA is selfsigned, IntrermediateCA1 is signed by RootCA, etc.)

RootCA -> IntermediateCA1 -> Client1 
RootCA -> IntermediateCA2 -> Client2

I want to use in nginx "IntermediateCA1", to allow access to site only to owner of the "Client1" certificate.

When I put to "ssl_client_certificate" file with IntermediateCA1 and RootCA, and set "ssl_verify_depth 2" (or more) , clients can login to site both using certificate Client1 and Client2 (should only Client1). The same result is when I put to "ssl_client_certificate" file with only RootCA - both clients can login.

When I put to "ssl_client_certificate" file with only IntermediateCA1, and set "ssl_verify_depth 1" (or "2" or more - no matter) , it is imposible to log in, I get error 400. And in debug mode i see logs:

verify:0, error:20, depth:1, subject:"/C=PL/CN=IntermediateCA1/[email protected]",issuer: "/C=PL/CN=RootCA/[email protected]"
verify:0, error:27, depth:1, subject:"/C=PL/CN=IntermediateCA1/[email protected]",issuer: "/C=PL/CN=RootCA/[email protected]"
verify:1, error:27, depth:0, subject:"/C=PL/CN=Client1/[email protected]",issuer: "/C=PL/CN=IntermediateCA1/[email protected]"
(..)
client SSL certificate verify error: (27:certificate not trusted) while reading client request headers, (..)

I thing this is a bug. Tested on Ubuntu, nginx 1.1.19 and 1.2.7-1~dotdeb.1, openssl 1.0.1. I see that nginx 1.3 has few more options about using client certificates, but I'dont see solution to this problem.

Currently, the only one way to separate clients 1 and 2 is to create two, selfsigned RootCAs, but this is only workaround..

Edit 1: I've reported this issue here: http://trac.nginx.org/nginx/ticket/301

Edit 2" *Ok, it's not a bug, it is feature ;)*

I get response here: http://trac.nginx.org/nginx/ticket/301 It is working, you must only check what your ssl_client_i_dn is (. Instead of issuer you can use also subject of certificate, or what you want from http://wiki.nginx.org/HttpSslModule#Built-in_variables

This is how certificate verification works: certificate must be verified up to a trusted root. If chain can't be built to a trusted root (not intermediate) - verification fails. If you trust root - all certificates signed by it, directly or indirectly, will be successfully verified.

Limiting verification depth may be used if you want to limit client certificates to a directly issued certificates only, but it's more about DoS prevention, and obviously it can't be used to limit verificate to intermediate1 only (but not intermediate2).

What you want here is some authorization layer based on the verification result - i.e. you may want to check that client's certificate issuer is intermediate1. Simplest solution would be to reject requests if issuer's DN doesn't match one allowed, e.g. something like this (completely untested):

[ Edit by me, it is working correctly in my configuration ]

server {
    listen 443 ssl;

    ssl_certificate ...
    ssl_certificate_key ...

    ssl_client_certificate /path/to/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    if ($ssl_client_i_dn != "/C=PL/CN=IntermediateCA1/[email protected]") {
        return 403;
    }
}
11
votes

Have you tried increasing ssl_verify_depth directive? Docs say:

(it) sets a verification depth in the client certificates chain.

But your verify depth is 1. You say:

I have experimented with setting ssl_verify_depth explicitly to 1 (as above) and then even 0 (not sure what that number means exactly), but still get same error.

So, try 2 or 3..

PS: Everywhere where I find this problem mentioned, its told to combine intermediate CA certificates with you server cert. into one file (as @vikas-nalwar suggested and you did) in order of verification (but i'm not sure if the order matters) and roughly speaking set ssl_verify_depth to number of certs in the bundle.

1
votes

@Jack and @HansL, a solution to allow clients from only one IntermediateCA1 is to use nginx config ssl_trusted_certificate. From nginx documentation:

Specifies a file with trusted CA certificates in the PEM format used to verify client certificates and OCSP responses if ssl stapling is enabled. In contrast to the certificate set by ssl client certificate, the list of these certificates will not be sent to clients.

These settings worked for me (but with other PKI of course). In the first file only IntermediateCA1 is present. In the latter only RootCA.

ssl_client_certificate     /etc/nginx/ssl/ca-bundle-for-client-selection-filtering.crt;
ssl_trusted_certificate    /etc/nginx/ssl/ca-bundle-for-client-cert-valdiation.crt;  
1
votes

Update: I believe that you want to enable client validation on your server side. If this is so then, create the certificate chain properly. Try the following in exact same order. Use the certchain.pem.

  cat intermediate.crt >> certchain.pem
  cat root.crt >> certchain.pem
0
votes

another easy way is to concatenate certificates (including domain certifate) in a single file and use that on your servers and nginx conf file

cat www.example.com.crt bundle.crt > www.example.com.chained.crt

Always remember to use server certificate first and then only CA server certificates

You can read more about at http://nginx.org/en/docs/http/configuring_https_servers.html#chains

0
votes

as I was strugling with nginx and cloudflare,
these lines did the trick for me:

ssl_client_certificate    /etc/nginx/ssl/ca-bundle-client.crt;  
ssl_verify_client optional_no_ca;  
ssl_verify_depth 2;

the second line with optional_no_ca is the important part

0
votes

I have to say its working fine for me with nginx/1.13.2, i.e.

  • I have one root CA that signed two intermediate CAs
  • both intermediates each signed a client
  • I concat the certs like cat client-intermediate1.crt ca-client.crt > ca.chained1.crt and cat client-intermediate2.crt ca-client.crt > ca.chained2.crt and cat ca.chained1.crt ca.chained2.crt > ca.multiple.intermediate.crt

  • if I only put ca.chained1.crt as ssl_client_certificate then only client1.crt can connect, likewise for ca.chained2.crt/client2.crt

  • when I use ca.multiple.intermediate.crt then both clients can connect

for revoking an intermediate, simply remove the cert chain from the ca.multiple.intermediate.crt

here is the relevant config. its also has high security settings

# minimum settings for ssl client auth 
ssl_client_certificate /etc/ssl/ca.multiple.intermediate.crt;
ssl_verify_client on;
ssl_verify_depth 2;

# ssl high security settings (as of writing this post)
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

if you want to parse out the certs CN and pass it on to backend, then add this OUTSIDE the server {.. block

# parse out CN
map $ssl_client_s_dn $ssl_client_s_dn_cn {
    default "should_not_happen";
    ~CN=(?<CN>[^,]+) $CN;
}

and INSIDE the block you can use it then

# add headers for backend containing SSL DN/CN
add_header X-SSL-client-s-dn $ssl_client_s_dn;
add_header X-SSL-client-s-dn_cn $ssl_client_s_dn_cn;
0
votes

ssl_verify_depth -> sets a verification depth in the client certificates chain

Authority provides a bundle of chained certificates which should be concatenated to the signed server certificate. The server certificate must appear before the chained certificates in the combined file

'$ openssl s_client -connect www.godaddy.com:443

...

Certificate chain
 0 s:/C=US/ST=Arizona/L=Scottsdale/1.3.6.1.4.1.311.60.2.1.3=US
     /1.3.6.1.4.1.311.60.2.1.2=AZ/O=GoDaddy.com, Inc
     /OU=MIS Department/CN=www.GoDaddy.com
     /serialNumber=0796928-7/2.5.4.15=V1.0, Clause 5.(b)
   i:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc.
     /OU=http://certificates.godaddy.com/repository
     /CN=Go Daddy Secure Certification Authority
     /serialNumber=07969287
 1 s:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc.
     /OU=http://certificates.godaddy.com/repository
     /CN=Go Daddy Secure Certification Authority
     /serialNumber=07969287
   i:/C=US/O=The Go Daddy Group, Inc.
     /OU=Go Daddy Class 2 Certification Authority
 2 s:/C=US/O=The Go Daddy Group, Inc.
     /OU=Go Daddy Class 2 Certification Authority
   i:/L=ValiCert Validation Network/O=ValiCert, Inc.
     /OU=ValiCert Class 2 Policy Validation Authority
     /CN=http://www.valicert.com//[email protected]'

In this example the subject (“s”) of the www.GoDaddy.com server certificate #0 is signed by an issuer (“i”) which itself is the subject of the certificate #1, which is signed by an issuer which itself is the subject of the certificate #2, which signed by the well-known issuer ValiCert, Inc. whose certificate is stored in the browsers’ built-in certificate base

Nginx use ssl_verify_depth directive to dig into the cert bundle to verify the issuer in its trusted store which is stored in proxy_ssl_trusted_certificate