We have a customer using Trac but whose security policy requires something stronger than reusable passwords. The enterprise has RSA SecurID token authentication systems. Past experience with the native RSA protocol has been excruciatingly painful: poor docs, difficult to use software libraries. Fortunately, the RSA server provides a RADIUS standard protocol access. I've used this in the past from within python applications using Wiggy's excellent pyrad module. For this, however, I didn't want to muck with the Trac code, and instead wanted to have Apache do the authentication. If this worked, it should work for any Apache-fronted webapp, and that could be a big win.
You may not have an RSA server, but RADIUS servers are quite commonly used for authentication. Their use seems to be increasing as a mechanism to provide authentication for handfuls of network devices. I have used FreeRADIUS for many years and like it, but there are others.
Local development on OS X
First I wanted to prototype it on my OS X laptop. FreeRADIUS will stand in for RSA's RADIUS server. Once the technology is proven to work, I'll reproduce it on the it production Linux server.
Install FreeRADIUS to emulate the RSA's RADIUS server since we have none on our dev LAN:
port install freeradius
Default install allows client localhost 127.0.0.1 with secret 'testing123'.
Create a user and password in /opt/local/etc/raddb/user:
tracuser Cleartext-Password := "tracpassword"
You don't need to supply any reply attributes like PPP address since we're not operating as a network access service. Ensure you don't have any indented lines right after this one.
Modern RADIUS auth and acct ports are 1812, 1813. Old ones are 1645, 1646.
Create some dirs the daemon uses by default:
mkdir -p /opt/local/var/log/radius mkdir -p /opt/local/var/run/radiusd
Run the daemon in foreground to watch for auth problems:
/opt/local/sbin/radiusd -f -X
Test client auth; specify the server on localhost by IPv4 address, else it tries the IPv6 address which didn't work; the port 666 here doesn't matter:
$ radtest tracuser tracpassword 127.0.0.1:1812 666 testing123 Sending Access-Request of id 170 to 127.0.0.1 port 1812 User-Name = "tracuser" User-Password = "tracpassword" NAS-IP-Address = 126.96.36.199 NAS-Port = 666 rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=170, length=20 $ radtest tracuser badpassword 127.0.0.1:1812 666 testing123 Sending Access-Request of id 19 to 127.0.0.1 port 1812 User-Name = "tracuser" User-Password = "badpassword" NAS-IP-Address = 188.8.131.52 NAS-Port = 666 rad_recv: Access-Reject packet from host 127.0.0.1 port 1812, id=19, length=20
If you've configured your server to live on the old RADIUS port, change this command appropriately.
Add mod_auth_radius to Apache
We can see that mod_auth_radius isn't already built into Apache:
On OS X SnowLeopard, the configs live in:
and it, like most Linuxes, includes other files:
The modules live in:
and sadly, there's no RADIUS module there.
Get the module source code from:
Specifically the file for Apache 2.0:
Download then install it into your Apache modules directory:
$ sudo apxs -i -a -c mod_auth_radius-2.0.c [...] /usr/share/httpd/build/instdso.sh SH_LIBTOOL='/usr/share/apr-1/build-1/libtool' mod_auth_radius-2.0.la /usr/libexec/apache2 /usr/share/apr-1/build-1/libtool --mode=install cp mod_auth_radius-2.0.la /usr/libexec/apache2/ cp .libs/mod_auth_radius-2.0.so /usr/libexec/apache2/mod_auth_radius-2.0.so cp .libs/mod_auth_radius-2.0.lai /usr/libexec/apache2/mod_auth_radius-2.0.la cp .libs/mod_auth_radius-2.0.a /usr/libexec/apache2/mod_auth_radius-2.0.a chmod 644 /usr/libexec/apache2/mod_auth_radius-2.0.a ranlib /usr/libexec/apache2/mod_auth_radius-2.0.a [...] chmod 755 /usr/libexec/apache2/mod_auth_radius-2.0.so [activating module `radius_auth' in /private/etc/apache2/httpd.conf]
Verify it's there:
$ httpd -M | grep radius Warning: DocumentRoot [/usr/docs/dummy-host.example.com] does not exist Warning: DocumentRoot [/usr/docs/dummy-host2.example.com] does not exist Syntax OK radius_auth_module (shared)
The 'apxs' command above added the line to the httpd.conf that included the module. Let's add a line to tell it to look in an external file to get the RADIUS configs, using the directory the httpd.conf file already includes. Put the following In /private/etc/apache2/other/radius.conf:
<IfModule radius_auth_module> AddRadiusAuth localhost:1812 testing123 5:3 AddRadiusCookieValid 2 </IfModule> <Location /secure/> AuthType Basic Order Allow,Deny Satisfy any Require valid-user AuthName "RADIUS authentication" AuthBasicAuthoritative Off AuthRadiusAuthoritative on AuthBasicProvider radius # Does this cookie REALLY expire the specified number of minutes? AuthRadiusCookieValid 1 AuthRadiusActive On </Location>
It appears to me that the cookie is getting set to 2 minutes, not the lower of the two settings as the docs suggest. It also seems that the cookie is never causing the session to time-out: it just keeps getting extended by 2 minutes on each page refresh, even if I waited more than 2 minutes to reload. This could be a show-stopper for sites that require automatic session time-out.
mod_auth_xradius: couldn't build
An alternate implementation, boasting:
- Distributed Authentication Cache using apr_memcache.
- Local Authentication Cache using DBM.
but I couldn't compile it; then again, I didn't specify the memcache but that doesn't appear to be the problem:
gcc -DHAVE_CONFIG_H -I. -I. -I./include -I./libradius -I/usr/local/include -DDARWIN -DSIGPROCMASK_SETS_THREAD_MASK -I/usr/include/apache2 -I/usr/include/apr-1 -I/usr/include/apr-1 -g -O2 -MT libradius_la-radlib.lo -MD -MP -MF .deps/libradius_la-radlib.Tpo -c libradius/radlib.c -fno-common -DPIC -o .libs/libradius_la-radlib.o libradius/radlib.c:64: error: static declaration of 'strsep' follows non-static declaration /usr/include/string.h:135: error: previous declaration of 'strsep' was here make: *** [libradius_la-radlib.lo] Error 1
and I don't feel like shaving that yak now.
I built Trac from a buildout I did for a different client.
Now I have to integrate it like our target Trac. Normally I integrate Trac into Apache with mod_wsgi, but this is not how our wiki server was built: it runs as a CGI.
I used the same trac.cgi as on the target box (see below), with some path changes, installed into /Library/WebServer/CGI-Executables and can now connect via Apache to Trac. Login fails of course because I've not configured it.
I then set the RADIUS auth as for standalone above:
<Location "/cgi-bin/trac.cgi/login"> AuthType Basic Order Allow,Deny Satisfy any Require valid-user AuthName "Trac RADIUS" AuthBasicAuthoritative Off AuthRadiusAuthoritative on AuthBasicProvider radius AuthRadiusCookieValid 1 AuthRadiusActive On </Location>
and it worked. W00t.
Target Linux Server
Now implement the above on the target Linux server.
- /etc/httpd/conf.d: There's a python.conf using mod_python for *.html but nothing trac specific
- /etc/httpd.conf: access restriction for /trac/login
- /var/log/httpd: needs privs to access
- Ours is httpd-2.2.3.
/usr/local/trac: drwxr-xr-x 4 tracuser tracuser 4096 Jan 11 04:51 . drwxr-xr-x 9 root root 4096 Jan 7 03:33 explorers drwxr-xr-x 9 apache tracuser 4096 Jan 12 08:03 helio -rw-r--r-- 1 kcunning kcunning 355 Feb 2 07:36 trac.htpasswd
Trac is integrated not as mod_python or mod_wsgi but as a CGI in /var/www/cgi-bin/trac.cgi.
Build, install the module after downloading it:
$ sudo /usr/sbin/apxs -i -a -c mod_auth_radius-2.0.c
Watchtower is now at:
watchtower.sef.hq.nasa.gov. 86400 IN A 184.108.40.206
Added the following stanzas to the httpd.conf:
<IfModule radius_auth_module> AddRadiusAuth 220.127.116.11:1645 radiussecret 5:2 AddRadiusCookieValid 2 </IfModule> <Location "/cgi-bin/trac.cgi/login"> AuthType Basic Order Allow,Deny Satisfy any Require valid-user AuthName "Trac RSA authentication, enter: PIN + PassCode" AuthBasicAuthoritative Off AuthRadiusAuthoritative on AuthBasicProvider radius AuthRadiusCookieValid 1 AuthRadiusActive On </Location>
And this worked fine, since my account was in the RSA server's user database. If our RSA server is configured to offer cross-realm authentication services, it can authenticate remote users as well. This is something we needed in our enterprise situation, and the RSA server hid the details so we didn't need to worry about it.
Only allow our users
I soon realized this config allowed anyone who could RSA authenticate to have access to our Trac: we were missing authorization. We could do that by creating a group in Trac and listing each user in that, but it seemed easier and more reliable to use Apache authentication against a group file. So I created /usr/local/trac/trac.htgroup:
rsatrac: username1 username2 username3
I then modified the Apache RADIUS and authentication stanzas above to require membership in the 'rsatrac' group
<IfModule radius_auth_module> AddRadiusAuth 10.9.8.7:1645 radiussecret 5:2 AddRadiusCookieValid 2 </IfModule> <Location "/cgi-bin/trac.cgi/login"> AuthType Basic Order Allow,Deny Satisfy any AuthGroupFile /usr/local/trac/trac.htgroup Require group rsatrac AuthName "Trac RSA authentication, enter: PIN + PassCode" AuthBasicAuthoritative Off AuthRadiusAuthoritative on AuthBasicProvider radius AuthRadiusCookieValid 1 AuthRadiusActive On </Location>
This achieved the desired effect. Our non-local users could authenticate (cross-realm), and RSA users not in our 'rsatrac' group of users could not gain access to the site.
RSA is OTP
RSA tokens are a One Time Password system. So if a users first authenticates to our VPN server to gain access to our network, they must wait for the token to generate a new code before using that code to authenticate to the Wiki.
No Indication of Token Problems
When the user can authenticate, all is fine.
However if authentication fails it could be for a number of reasons. It might be because the user typed in their passphrase wrong, or re-used one instead of waiting for a new token.
Or it may be because their token's been "locked" due to too many failures, or that the token's "expired". I believe (but cannot confirm) that the RSA RADIUS server returns different failure codes to indicate the condition. But even if it did, there is no mechanism in the mod_auth_radius to change behaviour based on this, nor does the HTTP Basic authentication mechanism have a way to relay an error status message to the user.
This can be confusing to users. It's probably best to educate users about this possibility, and how to recover. Ask the RSA folks where to got -- what server -- and what to do to recover from a locked or expired token.