Howto host git on your Linux box
Warning
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:
Install
giton the server:sudo apt-get install git-core git-doc
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
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
Set up a script that makes sure only relevant
gitcommands 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) andchmod a+xit#!/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:])
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
gitolder than v1.5, useinit-dbinstead ofinit)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/mygroupto 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
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_keysHow 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.comOr to allow only read-only access, add
--read-onlyas an option.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.
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
gitv1.5 or newer):git clone -o myserver git@myserver:myproject.git
They will probably want to set up
ssh-agentto 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.