Git: HTTPS Repository + Access Control

I’ve slowly switched to Git from Subversion over the last year or so, and lately I have begun to feel dissatisfied with my repository configuration. In this post, I’ll outline how to set up Git in a central repository model, exporting repositories over HTTP(S) and allowing for fine-grained access control.

Though it goes against the spirit of Git, I prefer the central repository model for two reasons. The first is backups. With a central repository, I only have to think about backing up one place to an off-site location. The second is access. My primary development system is a desktop, but I do occasionally take a laptop on the road. It’s easier to pull relatively recent source from a central, Internet-facing repository than it is to tunnel back into my network. I also do a lot of Rails work, and it’s simplest for Capistrano to pull from an Internet-facing repository.

So my goals are:

  • Public repositories are available over plain HTTP without authentication
  • Pushing into a public repository requires authenticated HTTPS
  • Private repositories are only available over HTTPS and require authentication for any access
  • The ability to allow a user access to only a subset of private repositories

Some details of the discussion will only be applicable to Debian/Ubuntu systems, but are easily adapted to other distributions.

The first step is creating a central repository.

$ cd /home/git
$ mkdir repo-name.git && cd repo-name.git
$ git --bare init
$ sudo chown -R www-data:www-data .

Next, you need to set up Apache to serve this repository. I use the smart HTTP transport supported by Git 1.6.6 and later, which is nearly as fast as the git:// protocol, but doesn’t require poking a hole in the firewall. Add this to your appropriate virtual host configuration.

<VirtualHost *:80>
        ServerName YOUR-SERVER-NAME
        ErrorLog /var/log/apache2/git-error.log
        CustomLog /var/log/apache2/git-access.log combined
 
        DocumentRoot /home/git
        SetEnv GIT_HTTP_EXPORT_ALL
        SetEnv GIT_PROJECT_ROOT /home/git
 
        # Let Apache serve static files
        AliasMatch ^/(.*/objects/[0-9a-f]{2}/[0-9a-f]{38})$          /home/git/$1
        AliasMatch ^/(.*/objects/pack/pack-[0-9a-f]{40}.(pack|idx))$ /home/git/$1
 
        ScriptAlias / /usr/lib/git-core/git-http-backend/
 
        <Directory /home/git>
                AllowOverride None
                Options +ExecCGI -Includes
                Order allow,deny
                Allow from all
        </Directory>
 
        <Location />
                Order deny,allow
                Deny from all
        </Location>
 
        <LocationMatch ^/public-repo.git>
                Allow from all
        </LocationMatch>
</VirtualHost>
 
# mod_authn_alias essentially configures groups of users.
<AuthnProviderAlias file internal>
        AuthUserFile /etc/apache2/git-internal.htpasswd
</AuthnProviderAlias>
 
<AuthnProviderAlias file clients>
        AuthUserFile /etc/apache2/git-clients.htpasswd
</AuthnProviderAlias>
 
<VirtualHost YOUR-SERVER-IP:443>
        SSLEngine on
        SSLCertificateFile PATH-TO-YOUR-CERTIFICATE-FILE
 
        ServerName YOUR-SERVER-NAME
        ErrorLog /var/log/apache2/git-error.log
        CustomLog /var/log/apache2/git-access.log combined
 
        DocumentRoot /home/git
        SetEnv GIT_HTTP_EXPORT_ALL
        SetEnv GIT_PROJECT_ROOT /home/git
 
        # Let Apache serve static files
        AliasMatch ^/(.*/objects/[0-9a-f]{2}/[0-9a-f]{38})$          /home/git/$1
        AliasMatch ^/(.*/objects/pack/pack-[0-9a-f]{40}.(pack|idx))$ /home/git/$1
 
        ScriptAlias / /usr/lib/git-core/git-http-backend/
 
        <Directory /home/git>
                AllowOverride None
                Options +ExecCGI -Includes
                Order allow,deny
                Allow from all
        </Directory>
 
        # Require authentication to all repositories
        <Location />
                AuthBasicProvider internal
                AuthType Basic
                AuthName "Git"
                Require valid-user
        </Location>
 
        # Allow both internal and client users to access this repository.
        <Location /client-repo.git>
                AuthBasicProvider internal clients
                AuthType Basic
                AuthName "Git"
                Require valid-user
        </Location>
</VirtualHost>

Create the password file. Remember to set user and group ownership and permissions appropriately.

$ sudo htpasswd -m -c /etc/apache2/git-internal.htpasswd <user>
$ sudo chown root:www-data /etc/apache2/git-internal.htpasswd
$ sudo chmod 640 /etc/apache2/git-internal.htpasswd

Repeat the htpasswd command once for each user, dropping the -c option (which creates the file, or truncates if it already exists) for subsequent users.

For each repository you want to export, you must run git update-server-info once by hand, and enable the post-update hook to do the same. On Debian, the sample post-update hook already does it, but it must be renamed to enable it.

$ cd /home/git
$ foreach r in *; do (cd $r; git update-server-info; \
    mv hooks/post-update.sample hooks/post-update); done

For any existing repositories that you push to, writes to those files is now done by Apache, so you must change the ownership of all of the files in the central repository to the web server user (www-data on Debian).

The final roadblock is that I use a wildcard, self-signed SSL certificate. Git refuses to accept these unless you approve it. There are two ways to do that.

  1. The simple, but insecure way: set http.sslVerify = false in your ~/.gitconfig. This turns off certificate verification for all HTTPS remotes.
  2. The more secure, complicated way: preface any command that talks to the remote (clone, push) with GIT_SSL_NO_VERIFY=1. Once you have a local copy, you can also edit its .git/config and add a new http section with the sslVerify = false setting.