Howto host git on your Linux box

Warning

This solution is obsolete. Use gitosis instead!

Gitosis is basically git-shell-enforce-directory's big brother, and an actual software project. Use it.

Updated to drop --use-separate-remote from git clone, it's the default.

Updated to add --read-only to git-shell-enforce-directory.

I've run repeatedly into cases where I want to provide services to people without really trusting them. I do not want to give them shell access. I don't want to even create separate unix user accounts for them at all. But I do want to make sure the service they use is safe against e.g. password sniffing.

Instead of trying to run the version control system over HTTPS (like Subversion's mod_dav_svn that will only work with Apache, which I don't run), I want to run things through SSH. SSH is the de facto unix tool for securing communications between machines.

Now, I said I don't want to create a unix user account for every developer using the version control system. With SSH, this means using a shared account, usually named by the service it provides: svn, git, etc. To identify different users of that account, do not give the account a password, but use SSH keys instead. To avoid giving people full shell access, use a command="..." when adding their public key to ~/.ssh/authorized_keys.

For Subversion, I submitted an enhancement to add --tunnel-user, to make sure the commit gets identified as the right user, and then used command="..." with the with arguments, like this (all on one line):

command="/srv/example.com/repo/svn/svnserve -t
  --root /srv/example.com/repo/svn/view/examplegroup
  --tunnel-user jdoe" ssh-rsa ... jdoe@example.com

Where the view directory is a bunch of symlinks to the actual repositories, allowing me to do group-based access control.

With git, the author of the changeset is recorded way before the SSH connection is opened. Without building some sort of access control in git hooks on the server, every developer can pretty much ruin the repository by overwriting branches with bogus commits. What they will not have is access outside of the repository, or a way to actually remove the old commits from the disk (unless you run git prune on the server). The distributed nature of git makes this reasonably easy to detect, and pretty much trivial to recover from. For any real trust in the code, you should look at signed tags anyway. The included wrapper allows you to have read-only users, but provides no detailed access control against developers with write access; they just won't be able to escape to the rest of the filesystem.

So, with that introduction out of the way, let's get to configuring:

  1. Install git on the server:

    sudo apt-get install git-core git-doc
    
  2. Create the directory structure store the repositories and related files

    sudo install -d -m0755 \
         /srv/example.com/repo/git \
     /srv/example.com/repo/git/.ssh \
     /srv/example.com/repo/git/repos \
     /srv/example.com/repo/git/view
    
  3. Create the shared user account for this service:

    sudo adduser \
         --system \
     --home /srv/example.com/repo/git \
     --no-create-home \
     --shell /bin/sh \
     --gecos 'git version control' \
     --group \
     --disabled-password \
     git
    
  4. Set up a script that makes sure only relevant git commands can be run via SSH, and to limit the visible section of the filesystem to things you actually want to give access to; put this file in /usr/local/bin/git-shell-enforce-directory (download) and chmod a+x it

    #!/usr/bin/python
    
    # Copyright (c) 2007 Tommi Virtanen <tv@eagain.net>
    #
    # Permission is hereby granted, free of charge, to any person
    # obtaining a copy of this software and associated documentation files
    # (the "Software"), to deal in the Software without restriction,
    # including without limitation the rights to use, copy, modify, merge,
    # publish, distribute, sublicense, and/or sell copies of the Software,
    # and to permit persons to whom the Software is furnished to do so,
    # subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be
    # included in all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
    # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.
    
    # Enforce git-shell to only serve repositories
    # in the given directory. The client should refer
    # to them without any directory prefix.
    # Repository names are forced to match ALLOW.
    
    
    import sys, os, optparse, re
    
    
    def die(msg):
        print >>sys.stderr, '%s: %s' % (sys.argv[0], msg)
        sys.exit(1)
    
    
    def getParser():
        parser = optparse.OptionParser(
            usage='%prog [OPTIONS] DIR',
            description='Allow restricted git operations under DIR',
            )
        parser.add_option('--read-only',
                          help='disable write operations',
                          action='store_true',
                          default=False,
                          )
        return parser
    
    
    ALLOW_RE = re.compile("^(?P<command>git-(?:receive|upload)-pack) '[a-zA-Z][a-zA-Z0-9@._-]*(/[a-zA-Z][a-zA-Z0-9@._-]*)*'$")
    
    
    COMMANDS_READONLY = [
        'git-upload-pack',
        ]
    
    
    COMMANDS_WRITE = [
        'git-receive-pack',
        ]
    
    
    def main(args):
        os.umask(0022)
    
    
        parser = getParser()
        (options, args) = parser.parse_args()
        try:
            (path,) = args
        except ValueError:
            parser.error('Missing argument DIR.')
        os.chdir(path)
    
    
        cmd = os.environ.get('SSH_ORIGINAL_COMMAND', None)
        if cmd is None:
            die("Need SSH_ORIGINAL_COMMAND in environment.")
    
    
        if '\n' in cmd:
            die("Command may not contain newlines.")
    
    
        match = ALLOW_RE.match(cmd)
        if match is None:
            die("Command to run looks dangerous")
    
    
        allowed = list(COMMANDS_READONLY)
        if not options.read_only:
            allowed.extend(COMMANDS_WRITE)
    
    
        if match.group('command') not in allowed:
            die("Command not allowed")
    
    
        os.execve('/usr/bin/git-shell', ['git-shell', '-c', cmd], {})
        die("Cannot execute git-shell.")
    
    
    if __name__ == '__main__':
        main(args=sys.argv[1:])
    
  5. Create your first repository:

    cd /srv/example.com/repo/git/repos
    sudo install -d -o git -g git -m0700 myproject.git
    sudo -H -u git env GIT_DIR=myproject.git git init
    

    (with git older than v1.5, use init-db instead of init)

  6. Set up an access control group and give it access to that repository:

    cd /srv/example.com/repo/git/view
    sudo install -d -m0755 mygroup
    cd mygroup
    sudo ln -s ../../repos/myproject.git myproject.git
    

    You can also use subdirectories of view/mygroup to organize the repositories hierarchically.

    Note, one SSH public key will belong to exactly one group, but if necessary you can create a separate group for each account for absolute control.

    Note, access to repository implies write access to repository, at least for now. You could make

  7. Get an SSH public key from a developer and authorize them to access the group:

    cd /srv/example.com/repo/git
    sudo vi .ssh/authorized_keys
    

    How the developer generates their key is out of scope here.

    Add a line like this, with the public key in it (all on one line, broken up in the middle of word to make sure there is no misunderstanding about when to use a space and when not to):

    command="/usr/local/bin/git-shell-enforce-directory /srv/exampl
        e.com/repo/git/view/mygroup",no-port-forwarding,no-X11-forwar
        ding,no-agent-forwarding,no-pty ssh-rsa ... jdoe@example.com
    

    Or to allow only read-only access, add --read-only as an option.

  8. You can now push things to the repository with:x

    git push git@myserver.example.com:myproject.git mybranch:refs/heads/master
    

    Note that before the first push, your server-side repository will not contain even an initial commit, and can't really be cloned.

  9. Now the developer can clone the repository:

    git clone git@myserver:myproject.git
    

    or to avoid some behavior of older git that I consider confusing (needs git v1.5 or newer):

    git clone -o myserver git@myserver:myproject.git
    

    They will probably want to set up ssh-agent to avoid typing the passphrase all the time.

And you're done! Good luck with your adventures with git, and welcome to the 21st century and to distributed version control systems.

2008-03-19T22:02+02:00, originally published 2007-03-23T17:43-07:00