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 ... [email protected]
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 git
on 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
Limit access
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 <[email protected]>
#
# 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 git
older than v1.5, use init-db
instead of init
)
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
Authorize users
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 ... [email protected]
Or to allow only read-only access, add --read-only
as an option.
You can now push things to the repository with
git push gi[email protected]: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 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.