Mutual TLS authentication for Rails admin panel

Here’s the example nginx configuration file for the Mutual TLS authentication for admin panels article. If you need an Apache configuration, read on.

upstream app_server {
  server unix:/var/run/unicorn.sock fail_timeout=0;
}

server {
 listen 80;
 root /home/rails/rails_project/public;
 server_name _;
 index index.htm index.html;

 location / {
   try_files $uri/index.html $uri.html $uri @app;
 }
 location /admin {
   rewrite ^ https://$host$request_uri? permanent;
 }

 location ~* ^.+\.(jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|mp3|flv|mpeg|avi)$ {
 try_files $uri @app;
 }

 location @app {
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header Host $http_host;
   proxy_redirect off;
   proxy_pass http://app_server;
 }
}
server {
 listen 443 ssl;
 root /home/rails/rails_project/public;

 ssl_certificate /etc/nginx/certs/server/client-ssl.bauland42.com.crt;
 ssl_certificate_key /etc/nginx/certs/server/client-ssl.bauland42.com.key;
 ssl_client_certificate /etc/nginx/certs/ca/ca.crt;
 ssl_verify_client on;
 ssl_session_timeout 1d;
 ssl_session_cache shared:SSL:50m;

 # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
 # ssl_dhparam /path/to/dhparam.pem;

 # modern configuration. tweak to your needs.
 ssl_protocols TLSv1.1 TLSv1.2;
 ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
 ssl_prefer_server_ciphers on;

 # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
 # add_header Strict-Transport-Security max-age=15768000;

 # OCSP Stapling ---
 # fetch OCSP records from URL in ssl_certificate and cache them
 # ssl_stapling on;
 # ssl_stapling_verify on;

 ## verify chain of trust of OCSP response using Root CA and Intermediate certs
 # ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;

 # resolver <IP DNS resolver>;

 location / {
   try_files $uri/index.html $uri.html $uri @app;
 }

 location ~* ^.+\.(jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|mp3|flv|mpeg|avi)$ {
   try_files $uri @app;
 }

 location @app {
   proxy_set_header X-Client-Dn $ssl_client_s_dn;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header Host $http_host;
   proxy_redirect off;
   proxy_pass http://app_server;
 }
}
  • Generally: This is how a client-authenticated TLS handshake works.
  • On a subdomain: This works with the admin panel on a subdomain because the self-signed server certificate needs a fully-qualified domain name as the Common Name. As far as I can see there’s no way to use a another certificate (apart from the one you already have) in a path like /admin.
  • You can use the routing constraints to make the admin panel available at a subdomain.
  • Ciphers? I used the Mozilla SSL Configuration Generator with nginx 1.4.6 and OpenSSL 1.0.1f to generate the cipher suite configuration. It works with Apache, too.
  • The nginx ssl_client_certificate directive points to our own CA to verify the client certificates, despite the confusing name. In Apache that’s SSLCACertificateFile.
  • The nginx ssl_verify_client directive is on, meaning it’s required to send a client certificate. In Apache that’s SSLVerifyClient.
  • Apache: At the bottom of this article is an example Apache configuration.
  • Mixed content: If you don’t see a green security icon, the developer toolbar in Firefox will tell you if that’s due to mixed content (HTTP images or scripts in an HTTPS page).
  • Installation: Firefox makes it easy to install your own certificates: Preferences > Advanced > Certificates > View Certificates. We’ll need to import the CA („Authorities”) and client-side („Your Certificates”) certificates.
  • Revoke: We’re setting an X-Client-Dn header that will contain the subject’s details from the client certificate. It will have this format: /CN=Heiko Webers/emailAddress=<email> (plus any other details that you entered when creating the certificate). You can parse the name in Rails and use it revoke certificates by listing just the names that are allowed. You could also use certificate revocation lists (CRL) or the Online Certificate Status Protocol. But for just a tiny number of certificates, that seems like overkill. raise unless request.headers['HTTP_X_CLIENT_DN'] =~ %r(\A\/CN=(.+)\/) && ['Heiko Webers'].include?($1)
  • You can also use nginx if or SSLRequire in Apache to check the subject of the client cert instead. But probably Rails code is easier to deploy.
  • Distribution: Make sure to encrypt emails when you send the certificate to your colleague.
  • Expiry: More likely is the expiry of certificates, so set a reminder in your calendar to renew the certificates a week before they expire.