Date

I just finished reinstalling Jenkins on my server. The reason for reinstalling Jenkins is to move it inside an LXC container. Hopefully, this will result in a reduced attack surface as well as making it easier to keep track of the global state needed to build my projects.

I use TLS client certificates on all the services I host that will only be used by me or a very-limited group of technically-savvy people. I prefer client certificates because it removes the need to use passwords, session cookies, or other methods to track authentication state. Once the initial setup is done, client certificates are almost fully transparent to the user. Unfortunately, making client certificate authentication work with Jenkins behind a proxy is tricky and requires some hacks.

Jenkins has a plugin that authenticates users via client certificates. However, because nginx terminates TLS and sends plaintext requests to Jenkins, Jenkins obviously can't know about client certificates directly and must be passed this information some other way. From its source code, this plugin appears to get information about client certificates by querying an attribute javax.servlet.request.X509Certificate that is apparently somehow set by Jenkins' surrounding servlet container. Unfortunately, some extended Googling seemed to suggest that the standard way to forward this information is to use a protocol called AJP, and recent versions of Jetty, the servlet wrapper for Jenkins, no longer supports AJP. Supposedly, AJP support can be reobtained by making Jenkins use Apache Tomcat instead. However, because I am not really familiar with how servlets are supposed to work and because there are various claims on the Internet that AJP is slower than HTTP, I decided to look for an alternative solution instead.

The alternative that I decided to use instead is the Reverse Proxy Auth Plugin . This plugin is intended to be used with a reverse proxy that performs some form of HTTP authentication and then forwards a username to Jenkins via an extra X-Forwarded-User header. However, this plugin doesn't actually care how the reverse proxy determines a username as long as the proxy does send one, so we can use this plugin as long as we can get nginx to extract a username from the client certificate.

The certificates I use for my services have a subject like /OU=<computer>/CN=<username>/emailAddress=<email>. We therefore need to extract the CN component from the subject. Fortunately, we can use this snippet from here:

map $ssl_client_s_dn $ssl_client_s_dn_cn {
    default "";
    ~/CN=(?<CN>[^/]+) $CN;
}

This snippet needs to go outside a server block. Finally, to actually proxy to Jenkins, add this inside a server block:

ssl_client_certificate <ca for client certs>;
ssl_verify_client optional;

# Jenkins proxy
location /jenkins {
    if ($ssl_client_verify = FAILED) {
        # BAD cert (rather than no cert at all)
        return 403;
    }
    proxy_set_header X-Forwarded-User $ssl_client_s_dn_cn;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect http:// https://;
    proxy_pass http://<where Jenkins is>:8080;
}

(The if block doesn't seem to be strictly necessary as nginx seems to generate a 400 error for client certificates that don't verify, but I am keeping it just in case.)

Finally, set up Jenkins, change its location under "Configure System", ensure that it doesn't complain about the proxy setup, and change the "Security Realm" to "HTTP Header by reverse proxy" under "Configure Global Security". If it doesn't seem to be working, you can debug by navigating to the /jenkins/whoAmI/ URL.