Code Your Own Multi-User Private Git Server in 5 Minutes

Following on from last weeks post about Simple Two Factor SSH Authentication this post shows you how to use the same SSH trick to create a multi-user private git server. I believe the principles here can also be applied to mercurial or subversion.

I was recently working on a client project that we converted to git, we hired an agency to work on the front-end for the project and they had four users that needed access. I didn’t really want to create them individual accounts on the server so I started thinking how I could securely manage multiple-user access to a git repository running under a single git user without giving them shell access.

After a bit of research I identified two possible candidates gitosis and gitolite but they seemed overkill for what I was trying to achieve.

Setting up the environment

We’ll assume we have a git user on the server and create a test repository in the home directory

git init --bare testing.git

Now we need to add my SSH key in authorized_keys with rw (read-write) permissions

command="/usr/bin/gitserve richard rw",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAA...zzz me@example.com

The first part of that line holds all the magic, firstly we will be invoking the script /usr/bin/gitserve with two parameters, a user identifier and the permissions that anyone authorising with this key has.

Then there are some extra options to disable port-forwarding, X11 forwarding, agent-forwarding, and finally deny the user access to a shell (we’ll only be executing git commands for this user).

The implementation

I’ve chosen to implement this in ruby but any language would do. Put the following in /usr/bin/gitserve

#!/usr/bin/env ruby

# user and permissions are passed from authorized_keys

user = ARGV[0]
permissions = ARGV[1]
command = ENV['SSH_ORIGINAL_COMMAND']
abort unless user and permissions and command

# check the supplied command contains a valid git action

valid_actions = ['git-receive-pack', 'git-upload-pack']
action = command.split[0]
abort unless valid_actions.include? action

# check the permissions for this user

abort "read denied for #{user}" unless permissions =~ /r/
abort "write denied for #{user}" if action == 'git-receive-pack' and permissions !~ /w/

STDERR.write "user #{user} authorized\n"

# user made a valid request so handing over to git-shell

Kernel.exec 'git', 'shell', '-c', command

When you execute git against a remote SSH repository it uses one of two commands git-receive-pack or git-upload-pack so we first verify those commands since we don’t want our git users to be able to execute anything else.

Next we check what permissions the user has, read-only (r) or read-write (rw) and grant access depending on what action they are trying to perform.

Finally if the user is valid we pass execution to git-shell with the original command.

Testing it out

$ git clone git@myserver:testing.git
Cloning into testing...
user richard authorized
warning: You appear to have cloned an empty repository.
$ cd testing
$ touch README
$ git add .
$ git commit -m 'added readme'
$ git push origin master
user richard authorized
Counting objects: 3, done.
Writing objects: 100% (3/3), 213 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@myserver:testing.git
 * [new branch]      master -> master

That seems to work! We can now easily add more users with read-only or read-write permissions by just adding a new line to the authorized_keys file with the corresponding users public key.

Wrapping up

This solution is very simple and works for my scenario. It doesn’t allow you to decide per-repository who has access to what but since you know which user has authorized in the script and what repository they are trying to access (in the command) it would be trivial to add support for per-repository access.