In a recent project I crashed into a wall. At least for a couple of days that is. The requirement was to integrate the Request Tracker (aka RT) installation on CentOS 7 server with Nginx to a client’s company single sign-on solution. Which wasn’t LDAP. Or Active Directory. Or anything standard at all – a complete homegrown system.
First set of Google searches firmly suggested to use ngx_http_auth_request_module. The instructions seemed quite straightforward. However, I was struggling to make it work. The reason for that was trivial – Nginx web server shipped with CentOS 7 is compiled without this module:
# rpm -qi nginx Name : nginx Epoch : 1 Version : 1.6.3 Release : 6.el7 Architecture: x86_64 Install Date: Wed 05 Aug 2015 06:37:41 AM UTC Group : System Environment/Daemons Size : 1441031 License : BSD Signature : RSA/SHA256, Wed 08 Jul 2015 01:38:40 PM UTC, Key ID 6a2faea2352c64e5 Source RPM : nginx-1.6.3-6.el7.src.rpm Build Date : Fri 03 Jul 2015 12:44:51 PM UTC Build Host : buildvm-09.phx2.fedoraproject.org Relocations : (not relocatable) Packager : Fedora Project Vendor : Fedora Project URL : http://nginx.org/ Summary : A high performance web server and reverse proxy server Description : Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and IMAP protocols, with a strong focus on high concurrency, performance and low memory usage.
I do know how to build custom RPMs, but that would mean a much increased maintenance, which I wanted to avoid.
So, the next question is: are there any compiled modules that I can use to solve the problem?
# nginx -V nginx version: nginx/1.6.3 built by gcc 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC) TLS SNI support enabled configure arguments: --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_spdy_module --with-http_realip_module --with-http_addition_module --with-http_xslt_module --with-http_image_filter_module --with-http_geoip_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module --with-http_stub_status_module --with-http_perl_module --with-mail --with-mail_ssl_module --with-pcre --with-pcre-jit --with-google_perftools_module --with-debug --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic' --with-ld-opt='-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E'
If you look carefully enough, you’ll notice the ngx_http_perl_module. Great news! If I have Perl, I can do anything. A quick scan through the documentation confirms that I should be able to access the request object.
Fast forward a few hours and I still got nowhere. The Nginx part was working enough for me to have some test variables and code snippets. But in order to work with the request object, I had to have Nginx Perl module installed on the system. Which is not shipped with CentOS 7 as a package. And which was failing to compile during the CPAN installation. There was something or other about Nginx server not being compiled with sufficient parameters. Sorry, I forgot to log the error and I moved on since.
So, is that it? No elegant, low maintenance solution then? Nginx rebuild required? I was very reluctant to believe this. And I wasn’t in a hurry. So I stopped and got some sleep. And as it often happens, a midnight enlightenment occurred: look for another pre-compiled package for Nginx, don’t use the one from the CentOS project.
Is there one? Yes, of course! In fact, there is an official Nginx repository, which supports CentOS (among other distributions), provides a much newer version of Nginx (1.8.0 instead of 1.6.3) and is compiled with the ngx_auth_request_module!
Here is how it looks:
# rpm -qi nginx Name : nginx Epoch : 1 Version : 1.8.0 Release : 1.el7.ngx Architecture: x86_64 Install Date: Thu 09 Jul 2015 02:07:42 PM EEST Group : System Environment/Daemons Size : 910765 License : 2-clause BSD-like license Signature : RSA/SHA1, Tue 21 Apr 2015 07:44:08 PM EEST, Key ID abf5bd827bd9bf62 Source RPM : nginx-1.8.0-1.el7.ngx.src.rpm Build Date : Tue 21 Apr 2015 06:37:00 PM EEST Build Host : centos7-amd64-ovl Relocations : (not relocatable) Vendor : nginx inc. URL : http://nginx.org/ Summary : High performance web server Description : nginx [engine x] is an HTTP and reverse proxy server, as well as a mail proxy server. # nginx -V nginx version: nginx/1.8.0 built by gcc 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) built with OpenSSL 1.0.1e-fips 11 Feb 2013 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-mail --with-mail_ssl_module --with-file-aio --with-ipv6 --with-http_spdy_module --with-cc-opt='-O2 -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic'
Excellent! So, now on to the configuration … what do we need to do there?
First of all, RT configuration is a straightforward FastCGI setup, as per the documentation. Adding auth request handling and we have the following location block:
location / { auth_request /auth; auth_request_set $username $upstream_http_x_username; fastcgi_param REMOTE_USER $username; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_NAME ''; fastcgi_param PATH_INFO $uri; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_pass 127.0.0.1:9000; }
So, what’s happening here?
- Line 1 tells Nginx to authenticate every request using /auth sub-request. That will be a very simple GET request without the body – just the headers, expecting a 200 OK response status back for anything that authenticated successfully, and any other response status for an authentication failure.
- Line 2 tells Nginx to set $username variable to the value of the X-Username response header, coming from the auth sub-request. This is handy for those cases where you don’t only need to know if the user is successfully authenticated, but also who he or she is.
- Line 5 sets the REMOTE_USER to the returned $username value, so that the RT can pick it up.
- Line 28 tells Nginx where the FastCGI handler is.
Now, in the same server block we need to define the handling of the /auth location. Here your mileage may vary, so use the below as a starting point:
location = /auth { proxy_pass http://localhost:8080; proxy_pass_request_body off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Origin-URI $request_uri; proxy_set_header Content-Length '0'; }
Whether or not you have everything on the same host is irrelevant. I’m just keeping this example here for the next time where it won’t be. The important bits here are about the conversion of POST requests to GET requests. All your /auth requests will be GET, irrelevant of the original request. So dropping the body of the request and setting Content-Length to 0 sounds like a good idea. In fact, in my original configuration I didn’t have the reset of the Content-Length, which resulted in me pulling out hair for a few hours, trying to understand why requests from 127.0.0.1 to 127.0.0.1 timeout when executed by Nginx (hint: sub-request) and succeed when executed by me from the command line (hint: direct request).
Now, for my authentication mechanism I setup the simplest of PHP virtual hosts, sending all requests to a simple script. Here’s the shortest version purely for illustration:
<?php function getUsername() { // ... custom authentication } $username = getUsername(); if ($username) { header('X-Username: ' . $username, true, 200); } else { header('X-Username: ', true, 403); } ?>
We are almost done now. The last thing to do is to send non-authenticated users to the login page. To do so, in our RT’s virtual host configuration we add the error page handler like so:
error_page 403 = @login; location @login { rewrite ^.* https://secure.example.com/login; }
Don’t forget to start the Nginx server, FastCGI handler, and PHP FPM, if you opted for the local PHP solution like I did. Cross your fingers and try it out.
Obviously, your application should support external authentication via the REMOTE_USER. For the Request Tracker, you need to set the following your etc/RT_SiteConfig.pm:
Set($WebRemoteUserAuth, 1); Set($WebRemoteUserContinuous, 1); Set($WebFallbackToRTLogin, undef); Set($WebRemoteUserGecos, 1); Set($WebRemoteUserAutocreate, 1);
Overall, it took me a while to arrive at this configuration, and it might seem slightly complicated at first, but the more I look at it, the more I like. I think it’s elegant, flexible, and quite low maintenance. Authentication mechanism is external and not web server dependent. The script itself can be adjusted, as the company moves to more standardized solution. The Nginx package is pre-compiled and from a well trusted source. And even the login redirect is handled without any hacking around.
If you have any improvement suggestions, please feel free to let me know via comments.