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 byumask(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.
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 ingit-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.