Relentless Coding

A Developer’s Blog

How to Create a Shared Git Repo

In this post we will take a look at how to set up a shared Git repository. The goal is to have multiple different users be able to push to and pull from this repository. Security is provided by using Unix users and groups.

Why would you want a shared repository? The alternative, using the same user for all your repositories is bad security. If you want to grant a user access to one of your repositories, you grant them access to all. By leveraging Unix users and groups we can can implement least privilege: each user can only access the repos s/he has been granted access to.

Prerequisites

I will assume you have a “Git server” set up. This boils down to having a server with Git installed and users having access to this server over SSH. A simple Docker container with Git and OpenSSH installed will do.

Create User and Groups

Firstly, create a user that will own all the bare Git repositories:

root@server# useradd -m git

Secondly, add the users that need access to a particular repo:

root@server# useradd -m user1
root@server# useradd -m user2
root@server# groupadd --users user1,user2 go-notify

You should name the group after the repository. That way, you can grant users access to a repository by adding them to the group that conveniently has the same name as the group. A name such as shared would be bad, unless you write code for a share daemon. In this post, I will use a group go-notify to accompany the shared go-notify repository.

Let’s check our work:

root@server# getent passwd user1 user2
user1:x:12348:12348::/home/user1:/bin/sh
user2:x:12349:12349::/home/user2:/bin/sh
root@server# 
root@server# getent group go-notify
go-notify:x:60000:user1,user2

Create the Shared Repository

Let the git user create the shared repository:

git@server$ git init --bare --shared -b main go-notify.git

The --shared flag:

[specifies] that the Git repository is to be shared amongst several users. This allows users belonging to the same group to push into that repository. When specified, the config variable core.sharedRepository is set so that files and directories under $GIT_DIR are created with the requested permissions. When not specified, Git will use permissions reported by umask(2).

Specifically, this means that the owning group will get read + write permissions on all files and directories in the shared repository. It will also set the setgid permission on all directories, meaning that

files and subdirectories created within to inherit its group ownership, rather than the primary group of the file-creating process. Created subdirectories also inherit the setgid bit.

Wikipedia

Interlude: setgid Background

Normally, when you create a directory, the effective group of your user will be the owning group:

$ mkdir whose-is-it
$ ls -ld whose-is-it
drwxr-xr-x 2 stefan stefan 40 25 jan 14:23 whose-is-it

Things change when the setgid bit is set on a directory:

user$ mkdir shared-dir
root# groupadd somegroup
root# chgrp somegroup shared-dir
root# chmod g+s shared-dir

Note that only root can change the group ownership of a file if the effective user ID is not a member of the target group. The same goes for the setgid bit.

user$ ls -ld shared-dir
drwxrwsr-x 2 user somegroup 4096 Jan 25 13:25 shared-dir
user$ touch shared-dir/test.txt
user$ ls -l shared-dir
-rw-r--r-- 1 user somegroup 0 Jan 25 13:33 test.txt
user$ mkdir shared-dir/subdir
user$ ls -ld shared-dir/subdir
drwxr-sr-x 2 user1 somegroup 4096 Jan 25 13:34 test/subdir

This should make clear that setgid ensures that:

  • New files inherit group ownership
  • Subdirectories maintain setgid automatically
  • Group ownership does not revert to user’s EGID

Continue Setting Up Shared Git Repository

Now that we understand how the --shared flag works on git init, let’s check ownership and permissions on our Git repository:

git@server:~$ ls -ld go-notify.git/
drwxrwsr-x 7 git git 4096 Jan  1 15:48 go-notify.git//
git@server:~$ ls -l go-notify.git/
total 32
-rw-rw-r-- 1 git git   21 Jan  1 15:48 HEAD
drwxrwsr-x 2 git git 4096 Jan  1 15:48 branches/
-rw-rw-r-- 1 git git  126 Jan  1 15:48 config
-rw-rw-r-- 1 git git   73 Jan  1 15:48 description
drwxrwsr-x 2 git git 4096 Jan  1 15:48 hooks/
drwxrwsr-x 2 git git 4096 Jan  1 15:48 info/
drwxrwsr-x 4 git git 4096 Jan  1 15:48 objects/
drwxrwsr-x 4 git git 4096 Jan  1 15:48 refs/

Change group ownership of the shared repo to the shared group:

root@server# chgrp -R go-notify go-notify.git

[Apparently, Some Linuxes remove setgid after a chown(1) or chgrp(1). If so, you need to set it again:

root@server# find go-notify -type d -execdir chmod g+s {} \+

Remember that only root can do this as the git user is not member of the go-notify group.

Check ownership and permissions again:

git@server:~$ ls -ld go-notify.git/
drwxrwsr-x 7 git go-notify 4096 Jan  1 15:48 go-notify.git//
git@server:~$ ls -l go-notify.git/
total 32
-rw-rw-r-- 1 git go-notify   21 Jan  1 15:48 HEAD
drwxrwsr-x 2 git go-notify 4096 Jan  1 15:48 branches/
-rw-rw-r-- 1 git go-notify  126 Jan  1 15:48 config
-rw-rw-r-- 1 git go-notify   73 Jan  1 15:48 description
drwxrwsr-x 2 git go-notify 4096 Jan  1 15:48 hooks/
drwxrwsr-x 2 git go-notify 4096 Jan  1 15:48 info/
drwxrwsr-x 4 git go-notify 4096 Jan  1 15:48 objects/
drwxrwsr-x 4 git go-notify 4096 Jan  1 15:48 refs/

Push With Remote User

Remote user user1 can now create a file and push it:

user1@pc$ git init -b main repo
user1@pc$ cd repo
user1@pc$ echo 'This is user1' > user1.txt
user1@pc$ git add user1.txt
user1@pc$ git commit -m 'chore: initial commit by user1'
user1@pc$ git remote add origin ssh://user1@server:/home/git/go-notify.git/

Now we can push:

user1@pc$ git push -u origin main
user1@127.0.0.1's password:
fatal: detected dubious ownership in repository at '/home/git/go-notify.git'
To add an exception for this directory, call:

        git config --global --add safe.directory /home/git/go-notify.git
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

Designate Safe Directories

Oops. It turns out that Git is particular about which repositories (directories really) it will fetch from and push to. git-config(1) gives the reason:

safe.directory
These config entries specify Git-tracked directories that are considered safe even if they are owned by someone other than the current user. By default, Git will refuse to even parse a Git config of a repository owned by someone else, let alone run its hooks, and this config setting allows users to specify exceptions, e.g. for intentionally shared repositories (see the --shared option in git-init(1)).

Since we are creating a shared directory, for every user that is allowed to interact with it, set it as a safe.directory on the server:

root@server# su -l user1 -c 'git config --global \
    --add safe.directory /home/git/go-notify.git'
root@server# su -l user2 -c 'git config --global \
    --add safe.directory /home/git/go-notify.git'

Note that it will not work if you put a trailing forward slash after go-notify.git.

Alternatively, trust the directory system-wide:

root@server# git config --system \
    --add safe.directory /home/git/go-notify.git

This will write to the system-wide configuration at /etc/gitconfig.

Now we can push:

user1@pc$ git push -u origin main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 403 bytes | 403.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To ssh://server:/home/git/go-notify.git
 * [new branch]      HEAD -> main
 branch 'main' set up to track 'origin/main'.

Another User Can Clone and Push As Well

user2 can clone the repo and then push its own commit:

user2@pc:~$ git clone ssh://user2@server:/home/git/go-notify.git
user2@pc:~$ cd go-notify/
user2@pc:~$ cat > user2.txt
File written by user2
^D
user2@pc:~$ git add user2.txt
user2@pc:~$ git commit -m 'chore: first commit by user2'
user2@pc:~$ git push

Alternatives

Using git init --shared is one way of achieving shared repositories. As this leverages standard Unix groups, this is pretty simple. Another way is leveraging ACLs, as discussed in a previous blog post. This is more complex, but also more flexible.