Apache 2.2 authentication with mod_authnz_external

Posted by Jonathan

The last couple of hours I’ve been fighting with Apache 2.2 authentication. I must say, it is really a mess, hard to debug, and very frustrating. Many times I wished for Lighttpd to have SVN/DAV and authentication modules.

The scenario is simple, you have a server that hosts your subversion repository and you with to serve it through HTTPS for security and access control. Subversion repositories can be served through the svn+ssh access method but this is inflexible (you can not control access to certain branches of the repository at least with subversion <= 1.2) and not properly encrypted (AFAIK svn+ssh only encrypts the login/authentification but not the actual data stream) as you need SSH/shell access for every user. So there is only the HTTPS access method left. And only Apache 2 as a server.

Because you need authentication for the subversion access the simplest thing to do is to create UNIX accounts on the server and let Apache authenticate with HTTPAuth over SSL against this database. You would think that the most common UNIX web server would be able to do this out of the box or at least with just one or two configuration knobs. But this is not the case, configuring Apache 2.2 to authenticate against UNIX shadow accounts is a major pain. Quite surprising if you look how easily dovecot (a POP/IMAP mail server, very young in comparison to Apache) does this out of the box.

There are are two recommended ways to do this, PAM (mod_auth_pam2) and using an external provider (mod_authnz_external). Both modules are not core modules and have their problems.

First there is PAM, the Pluggable Authentification Modules. The nice thing about PAM is that (apart from not being under active development any longer) it is itself a pain to configure and debug. Further in order to make mod_auth_pam(2) access to the shadow files you need to either run Apache as root (BIG MISTAKE!) or give the Apache user read access to the shadow files (also not a very good idea!). So after fighting with PAM for some time, I choose to try the second auth module, mod_authnz_external.

Mod_authnz_external hooks into the Apache auth systems and allows you to define external programs that get the username/password combination and return a status code. This way Apache can outsource the authentication process to an external program that has extra privileges and Apache can keep its low security profile. The author of mod_authnz_external ships such an external program, pwauth. Pwauth authenticates against UNIX shadow files or PAM (yea!). Pwauth runs as setuid root in order to do this but this is much better than running Apache as root or give Apache access to the shadow files.

At compile time you tell pwauth which users are allowed to use it (in my case the user www) and which auth backend to use (shadow or PAM). This can be tested like this (in (t)csh):

# sudo -u www pwauth ; echo $status 
username
correct_pw
0
# sudo -u www pwauth ; echo $status 
username
incorrect_pw
1
#

On FreeBSD the PAM config for pwauth looks like this:

# cat /etc/pam.d/pwauth
auth       required     pam_unix.so debug
account    required     pam_unix.so debug

(I thought that the same configuration would work for Apache, but it didn’t.)

When this is working for you, the fun part starts. Telling Apache how to use mod_authnz_external. In theory you just need to load it and explicitly use it in a part. But in reality it depends on other authz/authn modules and conflicts with others. Again not fun to debug. After a long period of trail and error I got it working. Comment out ALL authn/authz modules and load only these:

LoadModule auth_sys_group_module libexec/apache22/mod_auth_sys_group.so
LoadModule authnz_external_module libexec/apache22/mod_authnz_external.so
LoadModule auth_basic_module libexec/apache22/mod_auth_basic.so
LoadModule authz_host_module libexec/apache22/mod_authz_host.so

Now you can use it in your virtual host definitions. An important fact is that you need to redefine the external provider in every virtual host entry as it will not be inherited from the root node.

<VirtualHost  *:80>
  DocumentRoot "/usr/local/www/example.com/htdocs" 
  ServerName example.com

  <Directory "/usr/local/www/example.com/htdocs">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>

  <Location />
    SSLRequireSSL
    AuthType Basic
    AuthName "Restricted" 
    AuthBasicProvider external
    AuthExternal pwauth
    require valid-user
    require group my_group
  </Location>

  AddExternalAuth pwauth /usr/local/bin/pwauth
  SetExternalAuthMethod pwauth pipe
</VirtualHost>

The important entries are the external provides definition with name of “pwauth” just before the closing VirtualHost and the “AuthBasicProvider external && AuthExternal pwauth” inside the Location.

This finally works as wanted. I still can not believe that UNIX auth is so hard with Apache 2.2.

With SSL and mod_dav_svn the virtual host looks like this:

<VirtualHost _default_:443>
  DocumentRoot "/usr/local/www/example.com/htdocs" 
  ServerName example.com:443

  <Directory "/usr/local/www/example.com/htdocs">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>

  SSLEngine on

  SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL

  SSLCertificateFile /etc/ssl/certs/cert.pem
  SSLCertificateKeyFile /etc/ssl/private/cert.key

  SSLOptions +StdEnvVars +OptRenegotiate

  BrowserMatch ".*MSIE.*" \
      nokeepalive ssl-unclean-shutdown \
      downgrade-1.0 force-response-1.0

  <Location />
    SSLRequireSSL
    DAV svn
    SVNPath /usr/local/svn/my_repo
    AuthType Basic
    AuthName "Restricted" 
    AuthBasicProvider external
    AuthExternal pwauth
    require valid-user
    require group my_group
  </Location>

  AddExternalAuth pwauth /usr/local/bin/pwauth
  SetExternalAuthMethod pwauth pipe
</VirtualHost>

 

Mongrel and Rails behind Apache 2.2 and SSL

Posted by Jonathan

For a new project of mine we needed to operate Rails with HTTPS. Our setup is the same as I described in an earlier article about Mongrel and Apache 2.2 mod_proxy_balancer, so we have Apache 2.2 in front of a cluster of Mongrels.

After the initial plain HTTP setup was working fine we went on to configure HTTPS. The obvious way is to configure an Apache SSL virtual host, that proxies all requests to the Mongrel cluster (for more on how to setup the Mongrel cluster look here).

<VirtualHost _default_:443>
ServerName www.example.com:443
ServerAdmin webmaster@example.com
TransferLog /var/log/www/www.example.com/apache_ssl_transfer_log
ErrorLog /var/log/www/www.example.com/apache_ssl_error_log
CustomLog /var/log/www/www.example.com/apache_ssl_access_log combined

ProxyPass / balancer://mongrelcluster/
ProxyPassReverse / balancer://mongrelcluster/

SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
SSLCertificateFile /etc/ssl/example.crt
SSLCertificateKeyFile /etc/ssl/private.key

<FilesMatch "\.(cgi|shtml|phtml|php)$">
    SSLOptions +StdEnvVars
</FilesMatch>
BrowserMatch ".*MSIE.*" \
         nokeepalive ssl-unclean-shutdown \
         downgrade-1.0 force-response-1.0
CustomLog /var/log/httpd-ssl_request.log \
          "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" 
</VirtualHost>

This setup works fine until you initiate an internal redirect in your rails code like this:

redirect_to :action => 'list'

As Rails does not know that is behind an HTTPS proxy it creates a redirection to a HTTP resource. This breaks your security and e.g. results in IE complaining about unsafe file transmission on POSTs. James Duncan Davidson has a nice solution for this annoyance.

The solution is to tell Rails that it is operated in HTTPS mode without breaking the development environment. This can be done by setting an environment variable with Apache in the request and checking for this variable in a before filter. If this variable is set, redirect to HTTPS resources. Otherwise use plain old HTTP.

In order to set an environment variable in Apache, include the following line in the SSL virtual host definition:

RequestHeader set X_ORIGINAL_PROTOCOL 'https'

Now create a before_filter in the ApplicationController that checks for this variable:

before_filter :set_ssl
...

def set_ssl
  if request.env.has_key? 'HTTP_X_ORIGINAL_PROTOCOL'
    if request.env['HTTP_X_ORIGINAL_PROTOCOL'] == "https" 
      request.env["HTTPS"] = "on" 
     end
  end
end

request.env[“HTTPS”] = “on” tells Rails to consider the request as an HTTPS request and therefore generate redirects that obey this.

One thing to watch out for is that the variable gets a “HTTP_” prefix set by Apache. So we set the variable “X_ORIGINAL_PROTOCOL” but check for “HTTP_X_ORIGINAL_PROTOCOL”.

Knowing this can save you some hours of debugging…


UPDATE:
After poking around in the ActionController sources there seems to be a much better and easier way. Just set this variable (in httpd.conf) and delete the before_filter:

RequestHeader set X_FORWARDED_PROTO 'https'
Rails will figure out the rest itself. The magic comes from these lines in request.rb:
def ssl?
      @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end

FreeRADIUS SSL CA Annoyances

Posted by Jonathan

The last couple of days I’ve struggling with FreeRADIUS. Apart from not having a good documentation, a configuration mess, and not compiling out-of-the-box on OpenBSD, its OpenSSL CA scripts are broken.

FreeRADIUS ships a script that should help you set up your own CA for a self-signed certificate for EAP-TTLS. This script (CA.all in the scripts directory of the distribution) uses OpenSSL’s CA.pl script to do the SSL magic for you. The only problem is that the path to CA.pl is hardcoded:

SSL=/usr/local/ssl
export PATH=${SSL}/bin/:${SSL}/ssl/misc:${PATH}

...

echo “newreq.pem” | /usr/local/ssl/misc/CA.pl -newca

The hardcoded path will of course not work on OS X, FreeBSD or OpenBSD. As the good guys from FreeRADIUS do not use any type of error handling, this problem will manifest later with this error message:

Error opening CA private key ./demoCA/private/cakey.pem
1254:error:02001002:system library:fopen:No such file or directory:bss_file.c:278:fopen(’./demoCA/private/cakey.pem’,’r’)
1254:error:20074002:BIO routines:FILE_CTRL:system lib:bss_file.c:280:
unable to load CA private key

Took me a while to understand why the demoCA stuff was not created. The funny thing is that the path stuff would not work anyway as CA.pl is located in /usr/src/crypto/openssl/apps/ in FreeBSD and in /System/Library/OpenSSL/misc/ under OS X.

I hate developer that assume that UNIX = Linux.

lighttpd 1.4.1 released 0

Posted by Jonathan

lighttpd, the fast and lightweight webserver that is used by many rails sites, was updated to 1.4.1.

1.4.1 introduces WebDAV and support for nested conditionals in the configuration file.

Further some issue with SSL support on OpenBSD were fixed. I do not think that 1.4.1 will make it into the OpenBSD 3.8 ports tree as it now locked before the release.

I will write to the maintainer of the OpenBSD port and hope that the SSL issues will convince him and the ports people to update to 1.4.1 before 3.8.

UPDATE:
The maintainer told me that it is too late now in the release process to integrate the new version. Also the SSL support is still broken on 64bit archs.