Posts for the month of September 2013

Subversion and Trac setup with minimal privilege overlap

Over the years I developed (in discussion with a few colleagues and friends) a subversion and trac setup with a rather strong privilege separation. I've written it down for Debian wheezy, but it can be reproduced (with more or less effort) on other distributions as well. I once did it on SUSE, which was a nightmare, but this is more a judgment about the cleanness and flexibility of the distribution, not the setup described here. In addition some details will certainly need adjustments to new developments, but the basics are rather mature. Also, even when the focus of other setups vary, the whole concept is probably worth sharing. It also forced me to rewind my setup and write it down not in a bunch of snippets in a best practice folder on my machine (as I'm doing it all the time), but in a (still sparsely) documented blog post.

First of all, I will install trac from source as a regular user. As I frequently tweak details on the trac installation, I don't want to use the version that comes with the distribution. The important point here is, that trac will be installed neither as root nor as the user which runs the trac process in the end. On machines I'm managing myself (alone) I use my own user account for such installations, but you could use a specific software installation account. To make the installation process a little more universal, let us set an environment variable for this user (on a root shell we'll use in the course of this guide):

export SW=...

We install Apache, as it is still the solution of choice for serving subversion (webdav). We take subversion from the distribution as well as python and gunicorn (as a simple solution for serving trac):

apt-get install -y apache2 libapache2-svn python-virtualenv python-subversion python-dev gunicorn sudo

We don't want Apache to serve at Port 80, nor should it do anything else than subversion. As other use-cases of "specific" apache instances might come up, we disable the default configuration and setup an apache2-svn configuration. This apache instance will be run as a svn user to be created later on. The apache config is created by the following steps:

service apache2 stop
update-rc.d -f apache2 remove
echo -e '\necho "default apache disabled"\nexit' >> /etc/default/apache2
mkdir /etc/apache2-svn
cp /etc/apache2/{apache2.conf,magic} /etc/apache2-svn
sed -e 's/www-data/svn/' /etc/apache2/envvars > /etc/apache2-svn/envvars
echo "Listen 127.0.0.1:49443" > /etc/apache2-svn/ports.conf
ln -s /etc/apache2/{conf.d,mods-available} /etc/apache2-svn
mkdir /etc/apache2-svn/{mods-enabled,sites-available,sites-enabled}
APACHE_CONFDIR=/etc/apache2-svn a2enmod authz_host mime auth_basic authn_file authz_groupfile dav_fs authz_svn alias ssl
touch /etc/default/apache2-svn
sed 's/#.*apache2/&-svn/' /etc/init.d/apache2 > /etc/init.d/apache2-svn
chmod +x /etc/init.d/apache2-svn
mkdir /var/log/apache2-svn

As subversion uses more of http than simple get and post requests, it is rather hard to switch the scheme on a reverse proyx in front of the subversion server. Thus we create a private key, a signing request and self-sign it with our own private key:

openssl genrsa -out /etc/apache2-svn/server.key 2048
openssl req -new -batch -subj "/C=US/CN=localhost" -key /etc/apache2-svn/server.key -out /etc/apache2-svn/server.csr
openssl x509 -req -days 3650 -in /etc/apache2-svn/server.csr -signkey /etc/apache2-svn/server.key -out /etc/apache2-svn/server.crt

Now comes the apache configuration:

echo -e "ServerName svn\n"\
"ServerAdmin webmaster@$(hostname)\n"\
"DocumentRoot /var/www/\n"\
"CustomLog /var/log/apache2-svn/access.log combined\n"\
"SSLEngine On\n"\
"SSLCertificateFile /etc/apache2-svn/server.crt\n"\
"SSLCertificateKeyFile /etc/apache2-svn/server.key\n"\
"SSLSessionCache shmcb:/var/run/apache2-svn/ssl_scache(512000)\n"\
"SSLMutex file:/var/run/apache2-svn/ssl_mutex\n"\
"\n"\
"<Location /repos/>\n"\
"    DAV svn\n"\
"    SVNParentPath /var/lib/svn/repos\n"\
"    SVNListParentPath On\n"\
"    AuthzSVNAccessFile /etc/apache2-svn/authz\n"\
"    AuthType Basic\n"\
'    AuthName "svn repos"\n'\
"    AuthUserFile /etc/auth/htpasswd\n"\
"    Require valid-user\n"\
"</Location>\n"\
"\n"\
'RedirectMatch /repos$ /repos/' > /etc/apache2-svn/sites-available/default
APACHE_CONFDIR=/etc/apache2-svn a2ensite default

In this setup a subversion and trac user will be authenticated via a passwd file and the subversion access is configured in an authz file:

htpasswd -c /etc/$newuser/htpasswd user
echo -e "[groups]\n"\
"all=user\n"\
"\n"\
"[/]\n"\
"@all=rw" > /etc/apache2-svn/authz

Now we're all done regarding subversion except for the svn user itself:

adduser --system --home /var/lib/svn --group svn
sudo -u svn mkdir /var/lib/svn/{repos,backups,skel}
echo -e '#!/bin/bash\n'\
'/usr/bin/sudo -u trac /home/'$SW'/trac/bin/trac-admin /var/lib/trac/projects/NAME changeset added "$1" "$2" &&\n'\
'/usr/bin/svnadmin dump "$1" --revision "$2" --incremental|/bin/gzip > "/var/lib/svn/backups/NAME/dump.$(/usr/bin/printf %05d $2).gz"' > /var/lib/svn/skel/post-commit
echo -e '#!/bin/bash\n'\
'/usr/bin/sudo -u trac /home/'$SW'/trac/bin/trac-admin /var/lib/trac/projects/NAME changeset modified "$1" "$2" &&\n'\
'/usr/bin/svnadmin dump "$1" --revision "$2" --incremental|/bin/gzip > "/var/lib/svn/backups/NAME/dump.$(/usr/bin/printf %05d $2).gz"' > /var/lib/svn/skel/post-revprop-change
chown svn.svn /var/lib/svn/skel/*
chmod u+x /var/lib/svn/skel/*

Here we've included skeleton files for some hooks, which will create svn dump backups for each revision at checkin and inform trac about the changes in the repository. As trac will run as a separate trac user user, we need to add a sudoers rule:

echo "svn ALL=(trac)NOPASSWD:/home/wobsta/trac/bin/trac-admin /var/lib/trac/projects/* changeset added *,/home/wobsta/trac/bin/trac-admin /var/lib/trac/projects/* changeset modified *" > /etc/sudoers.d/trac
chmod 440 /etc/sudoers.d/trac

Ok, now let's install trac:

cd /tmp
sudo -u $SW virtualenv /home/$SW/trac
sudo -u $SW /home/$SW/trac/bin/pip install trac
sudo -u $SW mkdir /home/$SW/trac/wsgi
echo -e "#!/home/$SW/trac/bin/python\n"\
"\n"\
"import site, sys\n"\
"site.addsitedir('/home/$SW/trac/lib/python2.7/site-packages')\n"\
"from pkg_resources import working_set\n"\
"for path in sys.path:\n"\
"    working_set.add_entry(path)\n"\
"\n"\
"import os\n"\
"\n"\
"os.environ['PYTHON_EGG_CACHE'] = '/var/lib/trac/eggs'\n"\
"\n"\
"import trac.web.main\n"\
"def application(environ, start_response):\n"\
"    environ['SCRIPT_NAME'] = '/projects'\n"\
"    environ['REMOTE_USER'] = environ.get('HTTP_REMOTE_USER')\n"\
"    environ['trac.env_parent_dir'] = '/var/lib/trac/projects'\n"\
"    return trac.web.main.dispatch_request(environ, start_response)" | sudo -u $SW tee /home/$SW/trac/wsgi/projects.py > /dev/null

In the last step we've created a wsgi adapter. Due to the component architecture of trac adding the site-package path of the virtual env is not enough in this script, but package discovery requires a working_set modification as shown. In addition, we fix the SCRIPT_NAME environment variable and copy the HTTP_REMOTE_USER to the REMOTE_USER environment variable as required by trac. This setup enables us to use nginx as the front-end webserver in the end.

To complete the trac installation, we have to add the trac user to the system:

adduser --system --home /var/lib/trac --group trac
sudo -u trac mkdir /var/lib/trac/{projects,eggs}

Now we're left with starting apache2-svn and adding trac to the gunicorn configuration:

service apache2-svn start
update-rc.d apache2-svn defaults
service gunicorn stop
echo -e "CONFIG = {\n"\
"    'working_dir': '/home/$SW/trac/wsgi',\n"\
"    'environment': {\n"\
"    },\n"\
"    'user': 'trac',\n"\
"    'group': 'trac',\n"\
"    'args': (\n"\
"        '--bind=127.0.0.1:49080',\n"\
"        '--workers=3',\n"\
"        '--timeout=30',\n"\
"        'projects:application',\n"\
"    ),\n"\
"}" > /etc/gunicorn.d/trac
service gunicorn start

To create a new subversion repository and corresponding trac instance with a given name, execute:

export NAME=test
sudo -u svn svnadmin create /var/lib/svn/repos/$NAME
sudo -u svn mkdir /var/lib/svn/backups/$NAME
rm -r /var/lib/svn/repos/$NAME/hooks
cp -ra /var/lib/svn/skel /var/lib/svn/repos/$NAME/hooks
sed -i s/NAME/$NAME/ /var/lib/svn/repos/$NAME/hooks/*
sudo -u trac /home/$SW/trac/bin/trac-admin /var/lib/trac/projects/$NAME initenv

The reverse proxy configuration is straight forward and shown for as an nginx config snipped here

location /repos { proxy_pass https://127.0.0.1:49443; }

location /projects {
  rewrite ^/projects/(.*)$ /$1 break;
  proxy_pass http://127.0.0.1:49080;
}
location ~ ^/projects/[a-z]+/login {
  auth_basic "trac projects";
  auth_basic_user_file /etc/auth/htpasswd;
  proxy_set_header REMOTE_USER $remote_user;
  rewrite ^/projects/(.*)$ /$1 break;
  proxy_pass http://127.0.0.1:49080;
}

Thank's all, folks.