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.