Wednesday, April 8, 2015

NGINX + Apache Tomcat - Certificate Proxying Adventures

I ran into a problem a while back: I identified NGINX as the best technology to reverse proxy our Apache Tomcat instance, but there was 1 particular requirement that had no real solution at the time:
- Tomcat must use X509 Client Authentication


E.g.:


Client --<X509-Cert>->[ (Proxy w/SSL) ]--<X509-Cert>-->[ (Tomcat) ]


Why was that a problem?  If it was Apache HTTPD, it would be easy: use proxy_ajp and off you go.  It's a well documented configuration.


But this was NGINX, and it did not have a recommended AJP bridge which would pass through the appropriate SSL headers.  HTTP was the preferred pass through.  Even more odd, I could find no actual documentation of this configuration, though it seems like it would be more common (which it might be, but only for internal Enterprise deployments, where X509 is more ubiquitous).


I could not use the backend SSL connection, since Tomcat would pull out the Proxy servers' X509 certificate for authentication, and the backend web application was not developed with proxying of client certificates in mind, so no special headers (e.g. Proxy-User-DN) configurations would work.  I could have dug into the source and added it, but 1) the work would have to go through a slew of process and vetting for something I was not technically supposed to be doing, and 2) WHY DOESN'T THIS WORK!?  IT SHOULD WORK!


Which brings us to today (well, last week actually).  


First, let's explore this problem a bit.  This will provide some insight into the why's and how's that make up the (fairly simple) solution.


What do we really want to do?  In my case, while a secure connection is desired, it did not have to traverse past the Reverse Proxy (NGINX) - the backend was secured behind private networks and other defenses, and performance was a concern.  No, we just needed to present a valid client certificate to the Tomcat server, and the Spring Security framework would do the rest.


With a direct SSL connection, easy; no thought really.  All the necessary information is there because Tomcat was performing the SSL handshake and verification, and then adding the certificate, DNs, cipher, etc... to the context.  


Okay, so just send over the Client Cert to Tomcat, right?  NGINX has a $ssl_client_certificate variable.  


proxy_set_header SSL_CLIENT_CERT $ssl_client_certificate;  


Done, and done...


Well, no, that does not work.  


One, the HTTP connector does not transfer any SSL information into your context, so the SSL_CLIENT_CERT is ignored.


Two, even once you find the magical configuration that will grab the SSL information, it fails because it does not see the NGINX $ssl_client_certificate value as a valid PEM encoded certificate.


The first part of the solution; Apache Tomcat *can* pull out the SSL context with just a simple addition to your server.xml configuration: just add the SSLValve to your Engine configuration


The information in the link provides all the necessary details for setting it up on Tomcat and HTTPD's side.  It even shows setting the SSL_CLIENT_CERT header.


But, translating that to NGINX was necessary, and not as direct as one may think.  


NGINX actually instantiates 2 SSL variables for the client cert: $ssl_client_certificate, and $ssl_client_raw_cert.  


Yet, adding those headers via proxy_set_header, and trying it with either of those client_cert variables failed when the SSLValve attempted to decode them.


Hmmm...looking at that value in NGINX, or copying it out and running it through openssl x509 produces valid results...


The problem, it turns out, was hiding both behind the scenes, and in front of my face.


Take a look at the first comment in the invoke function of the org.apache.catalina.valves.SSLValve source (line 72 in the link):
   
/* mod_header converts the '\n' into ' ' so we have to rebuild the client certificate */


And then proceeds to add back in a line feed at every space.  In this case, the SSL certificate now has duplicate ‘\n’ characters, so the format becomes invalid.


SSLValve implicitly assumes you will be using Apache HTTPD...and all the nuances that come with it.


Really, it's right out there.  Tomcat and HTTPD are both Apache, so that is where the focus is for integration.  While I don't fault them for the implementation, a bit of explicit warning is probably in order.  I may even try to update the SSLValve implementation at some point if I get time.


Still, this actually provides the final part of our solution: get NGINX to send the CLIENT_CERT the same way mod_header does.  


Oh...NGINX does not have any built-in way to modify the variable value's when assigning them to the header...


But!  It does have a LUA plugin, which comes precompiled when you use OpenResty.  Not exactly the way I had hoped it would fall out, but that's all it took.  Have LUA modify the $ssl_raw_client_cert to translate all '\n' to ' ', and SSLValve accepts it, and Spring properly authenticates the user.  The actual change required, in addition to the headers required by SSLValve, to the location section of the config is:


set_by_lua $client_cert "return ngx.var.ssl_client_raw_cert:gsub('\\n',' ')";
proxy_set_header        SSL_CLIENT_CERT         $client_cert;


Success!  Tomcat can use the client cert for authorization, and we are no longer confined to HTTPD + AJP when using SSL and reverse proxies.

There are probably other ways to fix this up, but this method appears to be a good, general solution.


No comments:

Post a Comment

Disney's Cloudy Vision - Part 1

Today's Disney has the idea backwards: Disney Parks should be imagined as places where a particular character/IP would live, not create ...