You are here

Creating a simple git repository server (with ACLs) on FreeBSD

Not so frequently asked questions and stuff: 

The FreeBSD logoImage

Let's create a simple git server on FreeBSD.

It should:

  • Allow people to clone/pull/push using both SSH and HTTP.
  • Have a web view.
  • Have ACLs to allow repositories to only be visible and/or accessible by some specific users.

SSH interaction: gitolite

Let's install gitolite. It handles SSH connections and have the ACL functionality we're after.

First, here's a good read about how gitolite works: http://gitolite.com/gitolite/how.html#%281%29

On the git server

Install gitolite:

# make -C /usr/ports/devel/gitolite/ install clean

Copy your public key on the server, naming it [username].pub. That username will be considered the admin user.

Create a UNIX user that will own the files:

# pw useradd gitolite
# mkdir /home/gitolite
# chown gitolite:gitolite /home/gitolite
# cd /home/gitolite

Login as the UNIX user and initialize the system:

# sudo -s -u gitolite
% id
uid=1003(gitolite) gid=1003(gitolite) groups=1003(gitolite)
% /usr/local/bin/gitolite setup -pk admin.pub

Notice that the admin user can login using SSH, and that it will only execute gitolite's shell:

% cat .ssh/authorized_keys
command="/usr/local/libexec/gitolite/gitolite-shell admin",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa [some key]== zwm@git.example.net

That's all you need to do on the server.

On your client

Creating users and repositories

Clone the admin repository.

# git clone gitolite@git.example.net:gitolite-admin

Create two new keys (and thus users) and add them to the repository:

# ssh-keygen -t rsa -f erika
# ssh-keygen -t rsa -f jean

# cp erika.pub gitolite-admin/keydir
# cp jean.pub gitolite-admin/keydir

# git add keydir/jean.pub
# git add keydir/erika.pub
# git commit -m "Add users Jean and Erika."
# git push origin master

Create new repositories by setting their ACLs in the config file:

# cat conf/gitolite.conf:

repo gitolite-admin
    RW+     =   admin

repo testing
    RW+     =   @all

repo erika_only
    RW+     =   erika

repo erika_and_jean
    RW+     =   erika jean

# git add conf/gitolite.conf
# git commit -m "Add two new repos"
# git push origin master

Using the server

Try to clone repository erika_only with user jean:

# setenv GIT_SSH_COMMAND 'ssh -i jean'
# git clone gitolite@git.example.net:erika_only
Cloning into 'erika_only'...
FATAL: R any erika_only jean DENIED by fallthru
(or you mis-spelled the reponame)
fatal: Could not read from remote repository.

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

Our access was denied. ACLs are working.

Try to clone a ACL allowed repository:

# git clone gitolite@git.example.net:erika_and_jean
# cd 
# echo "Test" > test.txt
# git add test.txt
# git commit -m "Test commit"
# git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 218 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To gitolite@git.example.net:erika_and_jean
 * [new branch]      master -> master

Success.

HTTP interaction: nginx+git-http-backend

I assume you already know how to install and do the basic configuration of nginx.

Install fcgiwrap:

# make -C /usr/ports/www/fcgiwrap install clean

Configure fcgiwrap to use the right UNIX user:
/etc/rc.conf:

fcgiwrap_enable="YES"
fcgiwrap_user="gitolite"
fcgiwrap_profiles="gitolite"
fcgiwrap_gitolite_socket="tcp:198.51.100.42:7081"

Create a password file:

# cat /usr/local/etc/nginx/git_users.htpasswd
jean:$apr1$fkADkYbl$Doen7IMxNwmD/r6X1LdM.1
erika:$apr1$fOOlnSig$4PONnRHK3PMu8j1HnxECc0

Use openssl passwd -apr1 to generate passwords.

Configure nginx:

    server {
        [usual config here]

        auth_basic           "RESTRICTED ACCESS";
        auth_basic_user_file /usr/local/etc/nginx/git_users.htpasswd;
        client_max_body_size 256m;

        location ~ /git(/.*) {
            root /home/gitolite/;
            fastcgi_split_path_info ^(/git)(.*)$;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_param SCRIPT_FILENAME     /usr/local/libexec/gitolite/gitolite-shell;
            fastcgi_param QUERY_STRING $query_string;
            fastcgi_param REMOTE_USER        $remote_user;

            fastcgi_param GIT_PROJECT_ROOT    /home/gitolite/repositories;
            fastcgi_param GIT_HTTP_BACKEND /usr/local/libexec/git-core/git-http-backend;
            fastcgi_param GITOLITE_HTTP_HOME /home/gitolite;
            fastcgi_param GIT_HTTP_EXPORT_ALL "";

            # This include must be AFTER the above declaration. Otherwise, SCRIPT_FILENAME will be set incorrectly and the shell will 403.
            include       fastcgi_params;
            fastcgi_pass 198.51.100.42:7081;
        }
    }

Here we call gitolite-shell instead of git-http-backend directly to have gitolite check the users' permissions.

Let's clone a repository, add a commit and push it:

# git clone 'http://jean:lol@git.example.net:8080/git/erika_and_jean.git' erika_and_jean
Cloning into 'erika_and_jean'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Checking connectivity... done.

# cd erika_and_jean/
root@test:~/gitolite2/erika_and_jean # vim test.txt
root@test:~/gitolite2/erika_and_jean # git add test.txt
root@test:~/gitolite2/erika_and_jean # git commit -m "Pushed from HTTP"
[master 7604185] Pushed from HTTP
 1 file changed, 1 insertion(+)

# git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 258 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To http://jean:lol@git.example.net:8080/git/erika_and_jean.git
   fa03b7d..7604185  master -> master

Let's try to clone a repository we're not allowed to see:

# git clone 'http://jean:lol@git.example.net:8080/git/erika.git' erika
Cloning into 'erika'...
fatal: remote error: FATAL: R any erika jean DENIED by fallthru
(or you mis-spelled the reponame)

ACLs are working. Success.

Web view: GitWeb

Make sure git is compiled with option GITWEB.

Copy the gitweb files where nginx will look for them:

# cp -r /usr/local/share/examples/git/gitweb /usr/local/www/gitweb

Configure nginx:

      location / {
          root /usr/local/www/gitweb;
          index gitweb.cgi;

          location ~ ^/(.*\.cgi)$ {
              include  fastcgi_params;
              fastcgi_pass 198.51.100.42:7081;
              fastcgi_index gitweb.cgi;
              fastcgi_param SCRIPT_FILENAME /usr/local/www/gitweb/gitweb.cgi;
              fastcgi_param DOCUMENT_ROOT /usr/local/www/gitweb;
              fastcgi_param GITWEB_CONFIG /usr/local/etc/gitweb.conf;
              fastcgi_param REMOTE_USER        $remote_user;
          }
      }

No magic here. The Gitolite/GitWeb interaction is irrelevant to the webserver.

Use the gitolite command to find the values of the GL_ variables:

gitolite query-rc -a

Configure gitweb in /usr/local/etc/gitweb.conf:

BEGIN {
    $ENV{HOME} = "/home/gitolite";
    $ENV{GL_BINDIR} = "/usr/local/libexec/gitolite";
    $ENV{GL_LIBDIR} = "/usr/local/libexec/gitolite/lib";
}

use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;

$projectroot = $ENV{GL_REPO_BASE};
our $site_name = "Example.net Git viewer";

$ENV{GL_USER} = $cgi->remote_user || "gitweb";

$export_auth_hook = sub {
    my $repo = shift;
    # gitweb passes us the full repo path; we need to strip the beginning and
    # the end, to get the repo name as it is specified in gitolite conf
    return unless $repo =~ s/^\Q$projectroot\E\/?(.+)\.git$/$1/;

    # call Easy.pm's 'can_read' function
    return can_read($repo);
};

When connected as erika:

Image

When connected as jean:

Image

ACLs are working. Success.

Conclusion

Our users can now see, read and sometimes write into the repositories of our git server.

You can create guest accounts that will only be able to see specific repositories, and they won't even know the other ones are here.

No need to maintain a gitlab instance if your needs are simple.