Setting up a subversion repository over ssh ... properly!

Simon Tatham, the creator of the wonderful PuTTY ssh client for windows has written a fascinating article on his experiences converting his projects from CVS to subversion.

In the article he laments the sorry state of access models for subversion and gives some hints for how to build a proper access system to subversion using ssh (and, thus, ssh-agent). The article doesn't go into great detail about how exactly to set up the system using userv he describes, that being, I infer, left as an exercise for the reader. This page describes how I set it up.

Background: if you set up a subversion repository that more than one person is going to access, you have three options for making it available over the network: (i) run svnserve on a well-known port; (ii) use the subversion apache module; or (iii) mess around with permissions, groups, umasks and whatnot to share a repository's bare files between users who can log into the repository server. All these methods suck in different ways. Simon describes a great system using userv: you set up a repository as a non-privileged user, and then use userv to make the repository available (using svnserve) to some users who already have access to the machine.

The key assumption is that if somebody can ssh into the repository machine as a given user, then chances are good that you can assume that the person logging in is that user. That's authentication taken care of. You can take care of authorisation by having a list of users who are allowed access to your repository, either at the userv level or at the svnserve level, via (I believe) svnserve.conf. It so happens that I do it at the userv level. Doing authorisation via svnserve.conf would work too, possibly better, since you'd be able to give fine-grained access permissions to your repository.

Note that all the commands I give here have only been tested on a debian box. They might work on other unices and linuces.

Anyway, the first thing you have to do is set up an account on your machine, where you'll store your repository. Let's call the top of the repository "depot". You'll likely prefer a different name. And let's make the home directory for the subversion user by hand:

  # useradd subversion
  # mkdir /home/subversion
  # echo 'umask 077' >> /home/subversion/.profile
  # chown -R subversion.subversion /home/subversion
  # chmod -R og-rwx /home/subversion
  # su - subversion
  $ svnadmin create depot

We need to set up userv for the subversion user. We want to make the svnserve function available:

  $ mkdir .userv
  $ cat >> .userv/rc
  if ( glob service svnserve
     & grep calling-user /home/subversion/subversion-users
     )
          reset
          disconnect-hup
          require-fd 0 read
          require-fd 1 write
          require-fd 2 write
          no-suppress-args
          execute /home/subversion/svnserve-with-user
  fi
  ^D

We need to write this little svnserve-with-user script:

  $ cat >> /home/subversion/svnserve-with-userv
    #!/bin/sh -f
    exec svnserve -t --tunnel-user $USERV_USER $*
  ^D

And we need to make it executable:

  $ chmod +x /home/subversion/svnserve-with-userv

We also need to create a list of users allowed to access our repository. One user per line. (If you want all users to be able to access your repository, completely remove the line above in .userv/rc that refers to this subversion-users file.)

  $ echo "wesley" >> /home/subversion/subversion-users
  $ echo "wibble" >> /home/subversion/subversion-users

Now, if people want to access the repository, they have a problem. Most subversion clients have a remote access mechanism svn+ssh which ssh-es into a remote server and runs the command "svnserve -t". Unfortunately, this command is hard-coded into most ssh clients. Furrfu. There are two ways around this, both of which involve editing the [tunnels] section in your subversion configuration file.

The first way requires you to write a little program which knows how to ssh into the repository machine and knows to run 'userv subversion svnserve' instead of 'svnserve'. This is a great option, except I don't want people to have to create helper scripts on their local machine. Partly because I can't control what client and OS people will use. At least one of my users will use TortoiseSVN, and I don't want to be writing helper scripts for windows if I can avoid it. Life's too short.

I pondered this. I cursed the inflexibility of svn. Imagine hard-coding "svnserve -t" into your client and not making it possible to change it to something useful like "userv subversion svnserve". Dammit. Anyway, after much pondering, I realised that I could use the ForceCommand configuration option in recent openssh servers to make my openssh do whatever I wanted, and that I could run another ssh server on another port.

This ForceCommand option only works in recent version of openssh server. I think it was introduced in OpenSSH 4.4p1. If you have an earlier openssh, you'll need to upgrade it. Debian testing ("lenny") at the time of writing (September 2007) has a suitably recent openssh, but debian stable ("etch") does not.

Anyway, if you get another ssh server running on some port, I use 3691, that being one after the well-known port for raw svnserve, you can override the stupid "svnserve -t" thing built into svn clients.

This is the program I run from sshd:

  $ ^D
  # cat > /usr/local/bin/svnserve-proxy
    #!/bin/sh -f
    exec userv subversion svnserve
    ^D
  # chmod +x /usr/local/bin/svnserve-proxy

I happened to have daemontools already installed on my server. You can too. Just follow the instructions. Then you can get your new ssh running like this:

  # mkdir /etc/sshd-for-svnserve
  # cd /etc/sshd-for-svnserve
  # cat > run
    #!/bin/sh
    exec 2>&1
    echo "*** Starting sshd..."
    exec /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config -p 3691 -o 'ForceCommand /usr/local/bin/svnserve-proxy'
    ^D
  # chmod +x run
  # mkdir log
  # cat > log/run
    #!/bin/sh
    exec setuidgid multilog multilog t /var/log/sshd-for-svnserve
    ^D
  # chmod +x log/run
  # ln -s /etc/sshd-for-svnserve /service

You need to tell your users how to access the repository. If they're using the linux/unix ssh client, tell them to put a line in their .subversion/config file, in the [tunnels] section, like:

  [tunnels]
  ...
  ssh+3691 = $SVN_SSH_3691 ssh -p 3691

Users can then use a notation like this to refer to your repository

  svn co svn+ssh+3691://user@host/home/subversion/depot

If your users are using TortoiseSVN, then tell them to put a line like this in their subversion configuration file, which they can access from the TortoiseSVN settings page via an "Edit" button: (Note the capital "-P").

  [tunnels]
  ...
  ssh+3691 = $SVN_SSH_3691 tortoiseplink -P 3691

Setting up ssh-agent access to your repository with TortoiseSVN is a bit of a pain. You have to generate a key (with puttykeygen), put it in your .ssh/authorized_keys2 file on the server, run pageant, tell it about the key, set pageant to run at boot (and add the key again every time you log in) by dragging a link into your startup folder, and you probably also want to put a link into putty's list of hosts so you can use that to refer to a machine in your repository names. Sorry, this is a bit vague. I'll glady expand on this with screenshots if anybody actually reads this and wants me to write a bit more.

You can use git-svn too...

I've recently (December 2008) started playing with git-svn. I like the idea of The Repository being in svn, but having the local flexibility of git. (I'll likely change to using git for everything once I start to think like git does!) Once you've got the entry in your [tunnels] section per above, git-svn should just work.

Most - possibly all! - of the git-svn problems I've encountered have been due to me typing "git" instead of "git-svn".

In parting...

This page is almost definitely riddled with mistakes. I only wrote it. I didn't read it. Tell me if you notice anything odd.