I have a Raspberry Pi, which I use as a Git server. There are multiple physical users accessing it and currently each of these users has their own login on the server. In other words, the users John and Doe can login to the server with SSH by running ssh john@server.com
or doe@server.com
.
The physical users have private Git repositories, which no other user is able to access. E.g. John's repos are located in /home/john/repos
and Doe's repos are located in /home/doe/repos
on the server.
What I want is only one user called git
, which controls all users' repos. E.g, instead of John having the remote john@server.com:repos/project.git
, he would use git@server.com:john/project.git
. Similarly, Doe would push to git@server.com:doe/some_other_project.git
How can this be achieved while making sure that people cannot access each other's repos? The server is accessed via SSH.
The easiest solution is to set up someting like gitlab, which provides a web interface, various sorts of access control, and all sorts of other bells and whistles.
If you really want to roll your own:
Start with directions in the Git book for setting up your server to permit access to a shared user over ssh.
Those instructions will get you a shared git
account to which everyone has access, and would let anyone push to/pull from any repository. We can implement a simple authorization layer that will restrict users to repositories in particular directories.
Start with a small wrapper script:
#!/bin/sh
repo_prefix=$1
eval set -- $SSH_ORIGINAL_COMMAND
case "$1" in
(git-receive-pack|git-upload-pack|git-upload-archive)
# prevent attempts at using dir/../path to escape
# repository directory
case "$2" in
(*..*) echo "Invalid repository name." >&2
exit 1
;;
esac
repo_path="$repo_prefix/$2"
eval exec $1 $repo_path
;;
(*) echo "Unsupported command" >&2
exit 1
;;
esac
This will prepend a prefix path, provided as a command line argument, to all repository paths. Now, we need to arrange for this wrapper to intercept git operations.
To use this, you will need to modify the public keys you add to the git
user's authorized_keys
file. If you followed the instructions in the Git book, the authorized_keys
file will have one or more public keys in it, like this:
ssh-rsa AAAA...== some comment
For each public key, you will need to add a configuration option that will cause the wrapper script to be called in place of the original command. From the sshd
man page:
Each line of the file contains one key (empty lines and lines starting
with a ‘#’ are ignored as comments). Protocol 1 public keys consist
of the following space-separated fields: options, bits, exponent,
modulus, comment. Protocol 2 public key consist of: options, keytype,
base64-encoded key, comment. The options field is optional...
And slightly further down:
The following option specifications are supported (note that option
keywords are case-insensitive): [...]
command="command"
Specifies that the command is executed whenever this key is used for
authentication. The command supplied by the user (if any) is
ignored...The command originally supplied by the client is available
in the SSH_ORIGINAL_COMMAND environment variable.
With that in mind, we modify our authorized_keys
file to look something like this:
command="/usr/bin/git-wrapper.sh username" ssh-rsa AAAA...===
This means that when someone connects with the corresponding private key, sshd
will run git-wrapper.sh username
, causing our git-wrapper.sh
script to prepend repository paths with the string username
, ensuring that git
will only see repositories in the given directory. More specifically, when you run:
git push origin master
And assuming that origin
points to project.git
on the git server, thengit
will attempt to run on the remote server the command:
git-receive-pack project.git
Our wrapper script will intercept that, and transform it into:
git-receive-pack $1/project.git
So for example, if our git git
user home directory has no repositories:
git$ ls
And our authorized_keys
file looks like this:
git$ cat .ssh/authorized_keys
command="/usr/bin/git-wrapper.sh alice" ssh-rsa ... alice@example.com
command="/usr/bin/git-wrapper.sh bob" ssh-rsa ... bob@example.com
Then if alice
does this:
alice$ git remote add origin git@mygitserver:project.git
git push origin master
She will see:
fatal: 'alice/project.git' does not appear to be a git repository
fatal: Could not read from remote repository.
If we create the target repository:
git$ mkdir alice
git$ git init --bare alice/project.git
Then she can push:
alice$ git push origin master
[...]
To git@mygitserver:project.git
* [new branch] master -> master
But if bob were try to clone that repository:
bob$ git clone git@mygitserver:project.git
It would fail:
fatal: 'bob/project.git' does not appear to be a git repository
fatal: Could not read from remote repository.
Even if he tried something sneaky:
bob$ git clone git@mygitserver:../alice/project.git
Invalid repository name
fatal: Could not read from remote repository.
And that, in a somewhat verbose nutshell, is how you can access authorization for your git repository server. Note that this was all done for demonstration purposes only; you would want a substantially more robust script in a production environment.