14 May 2011

A secure, consistent SVN mirror

Using svnsync, ssh+svn, and post-commit hooks

Many svn mirror implementations take the hammer approach by running a cron job every minute to poll for changes. Bang, bang, bang and any change is pushed to a mirror via svnsync. But do you really want cron pounding away at subversion all the time and pulling changes out like a rusty nail?

Hammers and mirrors do not work well together.

The more elegant approach is to set up post-commit hooks. These capture every change as it happens. Unfortunately getting this to work is frustrating. Many administrators can’t get the post-commit to do anything at all; the post-commit “never runs” with no hint of failure and no logged errors.

Post-commit configuration usually fails due to permission problems. Each ssh+svn user comes in as themselves when a commit is being made, but that user often does not have permissions to a) run the post-commit hook and/or b) run commands in the post-commit script.

So here is how to both set up the mirror and set up the permissions so that post-commit can run.
This installation assumes CentOS/Redhat/AWS platforms and an already working svn repository where users access via ssh+svn.

Set up the svn mirror (aka target)

On the target system, create a user that will have exclusive write access to the mirror, traditionally this username is svnsync.
useradd svnsync
As the svnsync user, replicate the file system location of the original subversion repository. Then create the blank repository.
svnadmin create --fs-type=fsfs /repos/svn/myrepos
Normally when creating an SSH subversion repository, it is necessary to adjust the umask to 002 and create a group that will have write access. But in this case, only this user will have write privileges, so there is no need to create a subversion group, nor to adjust umask.

Then, if not already done, generate the ssh keys needed for the svnsync user to access the blank repository via SSH. For added security, add the following string to lock down the public key:
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/usr/bin/svnserve -t --tunnel-user=svnsync"
On the source system, add the svnsync user and the private SSH key. This user will read from the source and push changes to the target.

If a bit paranoid, a secondary check can be put on the target repository which ensures only the svnsync user can make a change in the form of a pre-revision change hook. This will prevent a non-SSH user from accidentally making commits.
cp -p pre-revprop-change.tmpl pre-revprop-change
Adjust the script to ensure only the svnsync user can effect a revision** change:
if [ "$USER" = "svnsync" ]
then exit 0
echo "Error: only the svnsync user can make changes" >&2
exit 1
Then enable the script: chmod 755 pre-revprop-change

Populate the mirror

Now the mirror is ready to be initialized and synchronized (a two step process). On the source system first initialize the mirror, which locks the target to the source repository. The init flag uses the <destination> <source> format:
svnsync init –username svnsync \ svn+ssh://svnsync@<mirror_server>/repos/svn/myrepos \ file:///repos/svn/myrepos
If this fails with the error “svnsync: Session is rooted”, double-check the SSH key. It likely has the repository path in the key: -r /repos/svn/myrepos. Remove this to generate a successful initialization.

The target is now locked to the source. Time to push the repository data. On the source svn server, synchronize the data:
svnsync synchronize --username svnsync \ svn+ssh://svnsync@<mirror_server>/repos/svn/myrepos
This may take some time, depending on the number of revisions in the repository and whether the mirror is on the local LAN or remote, such as on Amazon Web Services.

Congratulations, the source repository now has a read-only mirror.

However, maybe the network went down or something else happened during the first sync and now the re-sync attempt produces this error:
Failed to get lock on destination repos
Run the propdel command to remove the lock and restart the sync:
svn propdel svn:sync-lock --revprop -r 0 \ svn+ssh://svnsync@<mirror_server>/repos/svn/myrepos/
That should allow the sync to pick up where it stopped before.

Automate the mirror

Here is where the magic comes in. When a user makes a commit to the source repository via SSH, that process has the permissions of that user, not as root or some svn service. Unfortunately, the mirror only accepts writes from the svnsync user, not from everyone. This is where sudo comes in to solve the problem.
1. Set up SUDO privileges
On the source server, for SSH+SVN access, every user is already a member of a OS group that has read-write access to the repository. These group members must be given the ability to a) become the synsync user and b) run a single command. Fire up visudo and add this line:
%svngroup ALL=(svnsync) NOPASSWD: /usr/bin/svnsync
This allows all members of the svngroup (%svngroup) the ability to switch to the svnsync user (ALL=(svnsync)) without a password prompt (NOPASSWD) and run a single command (/usr/bin/svnsync).

Of course, users won’t be doing this manually. They won’t even know they have this “privilege”. If the SSH keys are locked down properly, users can’t log onto the subversion server, much less execute sudo commands remotely.

Instead, what this sudo change does is give the post-commit hook scripts the ability to push any changes to the mirror as the svnsync user.
2. Set up the post-commit scripts
Post-commit scripting is well documented, but without the sudo setup and sudo commands in the script, the post-commit scripts will not execute in a SSH+SVN environment.

On the source server, in the hooks folder, copy post-commit.tmpl and post-revprop-change.tmpl scripts:
cp –p post-commit.tmpl  post-commit
cp –p post-revprop-change.tmpl post-revprop-change
Add the svnsync commands to the post-commit script:
sudo -H -u svnsync /usr/bin/svnsync sync --non-interactive –-username svnsync svn+ssh://svnsync@<mirror_server>/repos/svn/myrepos &
exit 0
And also a slight variant to the post-revprop-change script:
sudo -H -u svnsync /usr/bin/svnsync sync --non-interactive --username svnsync copy-revprops svn+ssh://svnsync@<mirror_server>/repos/svn/myrepos "$REV" &
exit 0
If desired, something like >> /tmp/mirror-commit.log 2>&1 can be added in front of the ampersand so the mirror commits can be monitored.

Finally, enable the scripts with a chmod 755.

Congratulations. You now have an elegant, automated svn mirror!

1 comment:

Anonymous said...

I'm a little stunned that I'm the first to leave a comment. Getting svn+ssh to work with svnsync has been a nightmare. May I be the first to thank you for the useful guide.