How to put git on the web

Executive Summary

This document provides a walk-through to share (either with gitweb or as a remote for git clone and friends) your git repositories over HTTP/HTTPS with a variety of authentication methods: none, standard AuthUserFile Basic authentication, PAM authentication, LDAP authentication, and SSL Client Certificate authentication. This specifically is not a document on how to publish the contents of a git repository as a website.

TL;DR

Here are links to configuration files which let me work. Please note these are also the chapter headings. If you didn't read and it doesn't work, I don't want to hear about it. Contrariwise, if you did read the entire document and it doesn't work, I would like to hear about it.

ChapterFiles
Test environmentgit.conf
Simple gitweb over httpgit.conf gitweb.conf
Gitweb URL shorteninggit.conf
Anonymous read-only git http accessgit.conf gitweb.conf
Anonymous read-write git http accessgit.conf
Anonymous read-write git https accessgit.conf gitweb.conf
Adding https server certificate verification
Adding Basic AuthUserFile authenticationgit.conf
Switching to Basic PAM authenticationgit.conf
Switching to Basic LDAP authenticationgit.conf
Restricting LDAP access by groupgit.conf
Switching to SSL client certificate authenticationgit.conf
Restricting client certificates by groupgit.conf
Limited Cleanup, Disclaimers, Copyrights, Thanks, etc

Test environment

Created/tested on RHEL 6.2 virgin image launched from Amazon EC2. RHEL 6.2 at that time was running git 1.7.1 You should be able to exactly reproduce my steps if you use the same image, but in general should be able to reproduce my steps on any Unix/Linux system running Apache.

I chose /src as my directory of git repos. You of course may select any directory you want, replacing /src as necessary. I also chose http://<hostname>/g as my gitweb path, and http://<hostname>/G as my git URL base. The testing clone and software installation unpacking I did for a variety of reasons were done in ~/GOTW.

Note, the method I am are documenting is to add access to services and get them working first, and only after they are working do I add access restriction. This simplifies testing greatly since there are fewer moving parts. Thus it is strongly suggested to use some irrelevant test repos, like I did below, which will not matter if someone views, clones, or writes to them before access control is enabled. Of course, when you get to the stage of access control that you are comfortable with, you may stop at that time.

The RHEL 6.2 instance I was running of course had Apache with https installed. Additionally, I installed the "git" and "gitweb" packages.


yum install git gitweb
service iptables stop
service apache2 start
mkdir /src
chmod 755 /src
cd /src
git config --global user.email admin@example.com
git config --global user.name "Test user"
git clone --bare git://github.com/SethRobertson/GitBestPractices.git GitBestForks
git clone --bare git://github.com/SethRobertson/GitFixUm.git GitFixedForks
git clone git://github.com/SethRobertson/libbk.git libkfork
chown -R apache /src/*
mkdir ~/GOTW
cd ~/GOTW

As an alternative to stopping iptables you could enable port 80 and port 443 traffic and ensure that the system can talk to the LDAP server (or itself on the LDAP port if you fully follow the instructions below).

Simple gitweb over http

gitweb provides the ability to review recent changes in a repository, view diffs, file contents, branches, tags, and other useful read-only repository information via a web application. I strongly recommend setting gitweb up, to provide a stateless and portable method to examine arbitrary git repositories. It also provides a great linkage mechanism to link emails, bug tracking systems, wikis, to git repositories, which improves productivity through synergy.

Installing the gitweb RPM on RHEL 6.2 automatically creates /etc/httpd/conf.d/git.conf. This is the file we will be doing the majority of our work with.

However, I'm going to recommend adding "/g" as a shortcut to the default "/git" to shorten the link lengths.


echo "Alias /g /var/www/git" >> /etc/httpd/conf.d/git.conf

   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
 </Directory>
+Alias /g /var/www/git

This generates a revised git.conf, which you can also see by clicking on the green diff box, so we need to tell Apache about the configuration change:


service httpd reload

As I mentioned in the test environment section, I am placing the git repositories in /src, so I need to tell gitweb where to find those repositories.


mv /etc/gitweb.conf /etc/gitweb.conf.dist
echo 'our $projectroot = "/src";' > /etc/gitweb.conf

The new gitweb.conf is very simple, but simplicity is good. You can examine the /etc/gitweb.conf.dist file for other things you might want, but (with one exception I'll be using later) I've never had to use them myself.

Now you can try to visit http://`hostname`/g in a browser.

You should get your list of projects. If you do not, getting perhaps "404 - No projects found", as for example was the case on the RHEL 6.2 AWS instance I used for testing, perhaps you are encountering the delights of mandatory access control. If so, you can follow these SELinux boxes. If you are lucky enough to not have that problem, you can skip them.


yum install policycoreutils-python
semanage fcontext -a -t git_system_content_t  '/src(/.*)?'
restorecon -R -v /src

At this point, you should be seeing the list of projects, with "Unnamed" descriptions (you can name them by editing /src/*/description, not that it is worth it for these test projects.)

Gitweb URL shortening

However, before we abandon gitweb for greener grounds, one of the items espoused in git best practices is to integrate git commit messages with email, IRC, bug tracking, and the like. However, if you click your way down to a commit, you will see that the URL being generated is quite long.

http://`hostname`/g/?p=GitBestForks;a=commitdiff;h=fc2fd67834612c77a67a617f942a0202be4dbf4

You can of course use standard SHA shortening here:

http://`hostname`/g/?p=GitBestForks;a=commitdiff;h=fc2fd67

But even this is larger than it needs to be. Why not go for something more like:

http://`hostname`/g/GitBestForks/4fc2fd

Or if you had a non-bare repo, you can use something like:

http://`hostname`/g//libkfork/ab9f5f3


cat >> /etc/httpd/conf.d/git.conf <<'EOF'
RewriteEngine on
RewriteLog "/var/log/httpd/rewrite.log"
RewriteLogLevel 0
RewriteRule ^/g//([^/]+)/([0-9a-f]+)$ /g/?p=$1/.git;a=commitdiff;h=$2 [R,NE]
RewriteRule ^/g/([^/]+)/([0-9a-f]+)$ /g/?p=$1;a=commitdiff;h=$2 [R,NE]
EOF
service httpd reload

   DirectoryIndex gitweb.cgi
 </Directory>
 Alias /g /var/www/git
+RewriteEngine on
+RewriteLog "/var/log/httpd/rewrite.log"
+RewriteLogLevel 0
+RewriteRule ^/g//([^/]+)/([0-9a-f]+)$ /g/?p=$1/.git;a=commitdiff;h=$2 [R,NE]
+RewriteRule ^/g/([^/]+)/([0-9a-f]+)$ /g/?p=$1;a=commitdiff;h=$2 [R,NE]

You should now be able to visit the short (or long) URLs listed above and see useful content about those commits.

However, doing this is entirely optional and only useful if you have a post-receive hook which creates URLs for emails or other services and you desire for those URLs to be shorter. In other words, I strongly urge you to do this.

Anonymous read-only git http access

Now that we have gitweb access, it is time to add access to the git repositories. We will start with simple read-only access over HTTP.


cat >> /etc/httpd/conf.d/git.conf <<'EOF'
ScriptAlias /G/ /usr/libexec/git-core/git-http-backend/
<Directory "/usr/libexec/git-core/">
  SetEnv GIT_PROJECT_ROOT /src
  SetEnv GIT_HTTP_EXPORT_ALL
  Options +ExecCGI
  Order allow,deny
  Allow from all
</Directory>
EOF
service httpd reload

 RewriteLogLevel 0
 RewriteRule ^/g//([^/]+)/([0-9a-f]+)$ /g/?p=$1/.git;a=commitdiff;h=$2 [R,NE]
 RewriteRule ^/g/([^/]+)/([0-9a-f]+)$ /g/?p=$1;a=commitdiff;h=$2 [R,NE]
+ScriptAlias /G/ /usr/libexec/git-core/git-http-backend/
+<Directory "/usr/libexec/git-core/">
+  SetEnv GIT_PROJECT_ROOT /src
+  SetEnv GIT_HTTP_EXPORT_ALL
+  Options +ExecCGI
+  Order allow,deny
+  Allow from all
+</Directory>

semanage fcontext -a -t httpd_git_script_exec_t /usr/libexec/git-core/git-http-backend
restorecon -R -v /usr/libexec/git-core/git-http-backend

You should be able to git clone http://localhost/G/GitBestForks

Once again, this read-only clone access is unprotected. However, at least it is only read-only. Attempting to write gets "error: Cannot access URL http://localhost/G/GitBestForks/, return code 22" or perhaps a friendlier error message on a more recent git.

One last item. Now that we have http access to the repository, we can tell gitweb about it.


echo "our @git_base_url_list = ('http://`hostname`/G);" >> /etc/gitweb.conf

 our $projectroot = "/src";
+our @git_base_url_list = ('http://dev.example.com/G');

Anonymous read-write git http access

The next step is adding public write access.

Adding write access is actually quite easy, you just need to tell git what user is involved. Of course, since this is still anonymous, we are using a random name.


sed -i 's/^\( *SetEnv \)\(GIT_HTTP_EXPORT_ALL\)/\1\2\n\1REMOTE_USER anonymousweb/' /etc/httpd/conf.d/git.conf
service httpd reload

 <Directory "/usr/libexec/git-core/">
   SetEnv GIT_PROJECT_ROOT /src
   SetEnv GIT_HTTP_EXPORT_ALL
+  SetEnv REMOTE_USER anonymousweb
   Options +ExecCGI
   Order allow,deny
   Allow from all

semanage fcontext -d '/src(/.*)?'
semanage fcontext -a -t httpd_git_rw_content_t '/src(/.*)?'
restorecon -R -v /src

You can test this by modifying the clone we made in the previous section and attempting to push a change.


cd GitBestForks
echo >> index.md
git commit -am "test change"
git push
cd ..

Of course, now we have anonymous read-write access to our git repository exposed to the Internet. I hope you were paying attention when I suggested doing this on irrelevant repos.

Anonymous read-write git https access

Let's start adding some security. First off, let's restrict this to https only.


sed -i 's/^\( *\)\(Options.*\)/\1\2\n\1SSLRequireSSL/' /etc/httpd/conf.d/git.conf
service httpd reload

 <Directory /var/www/git>
   Options +ExecCGI
+  SSLRequireSSL
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
 </Directory>
   SetEnv GIT_HTTP_EXPORT_ALL
   SetEnv REMOTE_USER anonymousweb
   Options +ExecCGI
+  SSLRequireSSL
   Order allow,deny
   Allow from all
 </Directory>

rm -rf GitBestForks
git clone http://localhost/G/GitBestForks

This should generate an error message instead of a successful clone. We can try again using https and see if we are more successful.


GIT_SSL_NO_VERIFY=1 git clone https://localhost/G/GitBestForks

Good, we now have anonymous read-write access, over https (putting something over https automatically makes it more secure, right? Well, perhaps not, but we can actually add security soon.)

One last item. Now that we have changed git access from http to https, we can tell gitweb about it.


sed -i 's%http://%https://%;' /etc/gitweb.conf

 our $projectroot = "/src";
-our @git_base_url_list = ('http://dev.example.com/G');
+our @git_base_url_list = ('https://dev.example.com/G');

Adding https server certificate verification

You may have noticed the "GIT_SSL_NO_VERIFY=1" on the command line? Well, the system I was experimenting on didn't have a globally authorized and valid HTTPS certificate, so git aborted (with a particularly useless error message on this old RHEL version, modern git/libcurl's have more useful error messages).

If you have an officially issued SSL certificate (they can be cheap or even, with https://www.startssl.com, free) you don't need to use that environmental variable. You can also install the certificate authority certificate, which signed the https certificate, into git (either globally or locally) to avoid the problem in another way (for self-signed certificates, the common case for unofficial certificates, this is the https certificate itself).

We will go ahead with this process for the common case where you don't have a valid official certificate.


HTTPS_CERT=$(egrep -r ^SSLCertificateFile $(apachectl -V | grep HTTPD_ROOT | sed 's/.*\"\(.*\)\".*/\1/') | awk '{print $2;}')
openssl x509 -noout -issuer_hash -in $HTTPS_CERT
openssl x509 -noout -subject_hash -in $HTTPS_CERT

If the two hashes match, your certificate is self-signed and you may use it as the certificate authority certificate as well. If the hashes do not match, you must find your certificate authority certificate elsewhere. Once you find the certificate authority certificate, run (substituting the correct path):


mkdir ~/.ca
CA_CERT="$HTTPS_CERT"
cp $CA_CERT ~/.ca/ca.crt
git config --global http.sslCAInfo ~/.ca/ca.crt
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

If the clone fails at this point (and did work before with GIT_SSL_NO_VERIFY=1), your problem is most likely that the certificate subject common name (CN) doesn't match the hostname you used. You can try again with the proper hostname (and substitute that hostname wherever I say `hostname` in these instructions). To find the hostname the certificate was issued to, run:


openssl x509 -noout -subject -nameopt utf8,multiline -in /etc/pki/tls/certs/localhost.crt | awk '$1=="commonName" { print $3;}'

But it is entirely possible (probable if you are running on AWS like I did) that the certificate was issued to a hostname which will never work. If so, we can just generate a new one.

I will be using a certificate authority script I developed (a wrapper around openssl). It seems like everyone and their dog seems to have such a system, but none of the ones I have found have actually been easy to use for the purposes I wanted to (despite the name "easy" in some of their names). You of course may choose to generate your certificate some other way, use different paths, use different certificate subject names, etc:


git clone git://github.com/SethRobertson/CA-baka
CA-baka/CA-baka --workdir /etc/CA --country US --state NY --locality "New York" --organization "Examplz R Us" --newca "Examplz Private-Assurance CA" ""
CA-baka/CA-baka --workdir /etc/CA --newserver `hostname`
cp /etc/CA/ca.crt ~/.ca/ca.crt
cp $HTTPS_CERT $HTTPS_CERT.orig
cp /etc/CA/archive/`hostname`/server.crt $HTTPS_CERT
HTTPS_KEY=$(egrep -r ^SSLCertificateKeyFile $(apachectl -V | grep HTTPD_ROOT | sed 's/.*\"\(.*\)\".*/\1/') | awk '{print $2;}')
cp $HTTPS_KEY $HTTPS_KEY.orig
cp /etc/CA/archive/`hostname`/server.key $HTTPS_KEY
service httpd reload
git clone https://`hostname`/G/GitBestForks

After creating the new certificates issued to the correct hostname, update Apache to know about the new certificates, and copying the certificate authority certificate into the place we told git about, the git clone should definitely work.

Adding Basic AuthUserFile authentication

We will be adding basic AuthUserFile authentication now. However, I cannot actually recommend this as a good final configuration for you, since you will now have another source of access information and passwords which cannot be easily managed and will get forgotten and out of date.


sed -i '/REMOTE_USER/d' /etc/httpd/conf.d/git.conf
sed -i 's:\( *\)\(SSLRequireSSL\):\1\2\n\1AuthType basic\n\1AuthName "Private git repository"\n\1AuthUserFile /src/.git.passwd\n\1Require valid-user:' /etc/httpd/conf.d/git.conf
service httpd reload
htpasswd -b -c /src/.git.passwd user1 password1
htpasswd -b /src/.git.passwd user2 password2
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

 <Directory /var/www/git>
   Options +ExecCGI
   SSLRequireSSL
+  AuthType basic
+  AuthName "Private git repository"
+  AuthUserFile /src/.git.passwd
+  Require valid-user
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
 </Directory>
 <Directory "/usr/libexec/git-core/">
   SetEnv GIT_PROJECT_ROOT /src
   SetEnv GIT_HTTP_EXPORT_ALL
-  SetEnv REMOTE_USER anonymousweb
   Options +ExecCGI
   SSLRequireSSL
+  AuthType basic
+  AuthName "Private git repository"
+  AuthUserFile /src/.git.passwd
+  Require valid-user
   Order allow,deny
   Allow from all
 </Directory>

The clone above should have failed (or at least started asking your for a username/password if you have a newer (1.7.9 or later) git). We can then proceed with telling git (really libcurl) what the password is:


echo "machine `hostname` login user1 password password1" >> ~/.netrc
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

The clone should then work. But…you say you don't want to put your git password in plaintext in a file on disk? Don't you believe in the security of your system? If you have a recent enough version of git, you can use git's credential API to get the password from a native keychain. Recent enough is defined as git 1.7.9 or more later. Unfortunately, I didn't have that recent a version of git on my test vm, so I had to install it (and the required dependencies).


yum install curl-devel zlib-devel openssl-devel expat-devel
git clone git://github.com/gitster/git
cd git
make prefix=/usr/local install
cd ..
hash git
git --version
sed -i '/password1/d' ~/.netrc
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

Git should prompt you for the username and password, and you if you enter "user2" and "password2", the clone will work. You can explore using the other credential stores man gitcredentials on your own, but please do so only after finishing the rest of this walkthrough since having the system remember what credentials you are using will actually be counterproductive.

Switching to Basic PAM authentication

Well, if you remember, I recommended against using Basic AuthUserFile authentication because it forces a secondary management system to track all users. However, for those who allow their users to log into their system, the credentials are already stored in PAM. If we can hook Apache into PAM, we would eliminate the additional source of authentication data.

This can be done using "mod_authnz_external" and "pwauth", or "mod_authn_sasl", or "mod_auth_pam". Unfortunately none of the modules are available on my test system (RHEL 6.2) by default and the latter is officially deprecated in any event. We can go ahead and download, install, and use mod_authn_sasl as an example:


yum install httpd-devel
wget 'http://sourceforge.net/projects/mod-authn-sasl/files/latest/download?source=files'
tar xjf mod_authn_sasl-1.2.tar.bz2
cd mod_authn_sasl-1.2
./configure
make install
cd ..

In addition to the software, we also need to have a policy that PAM knows about:


cat >/etc/pam.d/http <<EOF
#%PAM-1.0
auth            include         password-auth
account         include         password-auth
EOF

Once that is ready, we can modify the configuration file to look at this provider.


sed -i 's:^\(Alias /git.*\):LoadModule authn_sasl_module modules/mod_authn_sasl.so\n\1:' /etc/httpd/conf.d/git.conf
sed -i 's:\( *\)AuthUserFile.*:\1AuthBasicProvider sasl:' /etc/httpd/conf.d/git.conf
service httpd reload

+LoadModule authn_sasl_module modules/mod_authn_sasl.so
 Alias /git /var/www/git

 <Directory /var/www/git>
   SSLRequireSSL
   AuthType basic
   AuthName "Private git repository"
-  AuthUserFile /src/.git.passwd
+  AuthBasicProvider sasl
   Require valid-user
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
   SSLRequireSSL
   AuthType basic
   AuthName "Private git repository"
-  AuthUserFile /src/.git.passwd
+  AuthBasicProvider sasl
   Require valid-user
   Order allow,deny
   Allow from all

This uses saslauthd to perform the authentication, which was installed on my test system by default. You obviously need to have saslauthd installed and running. If it is not running, then we can start it up. It takes the "-a pam" argument to use PAM as the backend, but that was configured as the default for me.


service saslauthd start

I needed to set up a user account for pam to work against. Presumably you will already have users.


adduser user3
echo password3 | passwd --stdin user3
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

cat > httpd_can_saslauthd.te <<EOF
module httpd_can_saslauthd 1.0;

require {
        type httpd_t;
        type saslauthd_var_run_t;
        type saslauthd_t;
        class dir search;
        class sock_file write;
        class unix_stream_socket connectto;
}

#============= httpd_t ==============
allow httpd_t saslauthd_var_run_t:dir search;
allow httpd_t saslauthd_var_run_t:sock_file write;
allow httpd_t saslauthd_t:unix_stream_socket connectto;
EOF
checkmodule -M -m -o httpd_can_saslauthd.mod httpd_can_saslauthd.te &&
semodule_package -o httpd_can_saslauthd.pp -m httpd_can_saslauthd.mod &&
semodule -i httpd_can_saslauthd.pp

When prompted, enter the new username/password (user3/password3) combination. It should work.

Switching to Basic LDAP authentication

PAM is nice and everything but perhaps you want to restrict access to your servers differently from the much larger set of people who have access to source code. Obviously you could write some complicated PAM policy, and of course, you can simply use saslauthd and point it to the LDAP backend instead of the PAM backend, but we could instead use a more standard (meaning installed by default for RHEL) LDAP specific policy.

Hopefully you already have an LDAP system set up, but that is not true for my test system, so I will first go through that exercise:


yum install openldap openldap-servers openldap-clients
find /etc/openldap/slapd.d -type f | xargs sed -i 's/dc=my-domain/dc=example/'
echo "olcRootPW: `slappasswd -s ldap_password`" >> /etc/openldap/slapd.d/cn\=config/olcDatabase\=\{2\}bdb.ldif
service slapd start

cat > /tmp/GOTW.ldif <<'EOF'
dn: dc=example,dc=com
objectclass: dcObject
objectclass: organization
o: Examplz R Us
dc: example

dn: cn=Manager,dc=example,dc=com
objectclass: organizationalRole
cn: Manager

dn: ou=People,dc=example,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit

dn: ou=Group,dc=example,dc=com
ou: Group
objectClass: top
objectClass: organizationalUnit

dn: cn=dev,ou=Group,dc=example,dc=com
objectClass: posixGroup
objectClass: top
cn: dev
userPassword: {crypt}x
gidNumber: 4004
memberUid: user5

dn: cn=users,ou=Group,dc=example,dc=com
objectClass: posixGroup
objectClass: top
cn: dev
userPassword: {crypt}x
gidNumber: 4003

dn: cn=sales,ou=Group,dc=example,dc=com
objectClass: posixGroup
objectClass: top
cn: sales
userPassword: {crypt}x
gidNumber: 4005
memberUid: user4

dn: uid=user4,ou=People,dc=example,dc=com
uid: user4
cn: user4
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: {crypt}$1$dKQtB.Ir$S9hUXtplYff.yw6T1UF3I0
shadowLastChange: 15155
shadowMin: 0
shadowMax: 99999
shadowWarning: 7
loginShell: /sbin/nologin
uidNumber: 4001
gidNumber: 4003
homeDirectory: /tmp
gecos: user4

dn: uid=user5,ou=People,dc=example,dc=com
uid: user5
cn: user5
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: {crypt}$1$dKQtB.Ir$a3ieZuk5h7bJ5B6/SyGI81
shadowLastChange: 15155
shadowMin: 0
shadowMax: 99999
shadowWarning: 7
loginShell: /sbin/nologin
uidNumber: 4002
gidNumber: 4003
homeDirectory: /tmp
gecos: user5

EOF
ldapadd -h localhost -w ldap_password -D 'cn=manager,dc=example,dc=com' < /tmp/GOTW.ldif

I've chosen mod_authnz_ldap as an apparently flexible LDAP authentication scheme.


sed -i '/authn_sasl/d' /etc/httpd/conf.d/git.conf
sed -i 's%^\( *\)AuthBasic.*%\1AuthBasicProvider ldap\n\1AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"%' /etc/httpd/conf.d/git.conf
service httpd reload
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

-LoadModule authn_sasl_module modules/mod_authn_sasl.so
 Alias /git /var/www/git

 <Directory /var/www/git>
   SSLRequireSSL
   AuthType basic
   AuthName "Private git repository"
-  AuthBasicProvider sasl
+  AuthBasicProvider ldap
+  AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
   Require valid-user
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
   SSLRequireSSL
   AuthType basic
   AuthName "Private git repository"
-  AuthBasicProvider sasl
+  AuthBasicProvider ldap
+  AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
   Require valid-user
   Order allow,deny
   Allow from all

Please enter "user4" and "password4" when you clone, which should work. Congratulations, you are now cooking with LDAP!

Restricting LDAP access by group

If were reading particularly carefully, you might have noticed in the LDIF above, I placed "user4" into the "sales" group. "user5" was the one placed into the "dev" group. How can we restrict access to only members of group dev?

Well, unfortunately there are like 15 billion different standards for LDAP groups (only a minor exaggeration). I will use the nis RFC2307 format for basic groups. Other group systems can be used as well, please read the authnz LDAP documentation.


sed -i 's%^\( *\)\(AuthLDAPURL.*\)%\1\2\n\1AuthLDAPGroupAttribute memberUid\n\1AuthLDAPGroupAttributeIsDn off\n\1Require ldap-group cn=dev,ou=Group,dc=example,dc=com%' /etc/httpd/conf.d/git.conf
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

   AuthName "Private git repository"
   AuthBasicProvider ldap
   AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
+  AuthLDAPGroupAttribute memberUid
+  AuthLDAPGroupAttributeIsDn off
+  Require ldap-group cn=dev,ou=Group,dc=example,dc=com
   Require valid-user
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
   AuthName "Private git repository"
   AuthBasicProvider ldap
   AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
+  AuthLDAPGroupAttribute memberUid
+  AuthLDAPGroupAttributeIsDn off
+  Require ldap-group cn=dev,ou=Group,dc=example,dc=com
   Require valid-user
   Order allow,deny
   Allow from all

If you enter "user4" and "password4", it should not work. Then try the last command again and enter "user5" and "password5", which should work.

Switching to SSL client certificate authentication

LDAP is all nice and everything, especially with keystore credential storage so you don't have to enter the username/password every time, but some people would prefer to use SSL certificate authentication instead. This can be a very convenient way to get access with a single-sign-on type approach, without requiring the overhead of setting up LDAP and (in some administrative domains) political problems inherent with adding users to the LDAP system. However, instead of setting up LDAP, you have to set up a user based certificate system.

Hopefully you already have properly signed x509 certificate already distributed to all users, but in case you do not, we will walk you through the process of setting it up.

If you did NOT create a server certificate using CA-baka earlier, then you need to run the following steps. If you did create the certificate, then skip over these commands. Please note that the client certificates do not need to be issued by the same authority as the web server certificate. You can have the web server certificate be official and the client certificates be self-generated.


git clone git://github.com/SethRobertson/CA-baka
CA-baka/CA-baka --workdir /etc/CA --country US --state NY --locality "New York" --organization "Examplz R Us" --newca "Examplz Private-Assurance CA" ""

Now you can issue certificates to some users.


CA-baka/CA-baka --workdir /etc/CA --organizationalunit "Sales" --newclient user6@example.com
CA-baka/CA-baka --workdir /etc/CA --organizationalunit "SCM" --newclient user7@example.com

Not too painful. The "--organizationunit" (OU) added to the client certificates is to distinguish developers from other random certificates which might have been issued by the same authority, but if you are only issuing developer certificates, you don't need to include that (or can include that in the --newca lines).

In the below example, I am setting SSLCACertificateFile in the git.conf for convenience, but normally one would configure it near the SSLCertificateFile definition, probably in ssl.conf. That configuration key should point at the certificate which issued the client certificates you are attempting to validate.


sed -i 's:^\(Alias /git.*\):SSLCACertificateFile /etc/CA/ca.crt\n\1:' /etc/httpd/conf.d/git.conf
sed -i -e '/^ *Auth/d' -e '/^ *Require/d' /etc/httpd/conf.d/git.conf
sed -i 's:\( *\)\(SSLRequireSSL\):\1\2\n\1SSLVerifyClient require\n\1SSLVerifyDepth 1\n\1SSLUserName SSL_CLIENT_S_DN_CN:' /etc/httpd/conf.d/git.conf
service httpd reload
cp /etc/CA/archive/user6@example.com/client.crt ~/.ca/user6.crt
cp /etc/CA/archive/user6@example.com/client.key ~/.ca/user6.key
git config --global http.sslCert ~/.ca/user6.crt
git config --global http.sslKey ~/.ca/user6.key
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

+SSLCACertificateFile /etc/CA/ca.crt
 Alias /git /var/www/git

 <Directory /var/www/git>
   Options +ExecCGI
   SSLRequireSSL
-  AuthType basic
-  AuthName "Private git repository"
-  AuthBasicProvider ldap
-  AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
-  AuthLDAPGroupAttribute memberUid
-  AuthLDAPGroupAttributeIsDn off
-  Require ldap-group cn=dev,ou=Group,dc=example,dc=com
-  Require valid-user
+  SSLVerifyClient require
+  SSLVerifyDepth 1
+  SSLUserName SSL_CLIENT_S_DN_CN
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
 </Directory>
   SetEnv GIT_HTTP_EXPORT_ALL
   Options +ExecCGI
   SSLRequireSSL
-  AuthType basic
-  AuthName "Private git repository"
-  AuthBasicProvider ldap
-  AuthLDAPURL "ldap://localhost:389/ou=People,dc=example,dc=com?uid?sub?(objectClass=*)"
-  AuthLDAPGroupAttribute memberUid
-  AuthLDAPGroupAttributeIsDn off
-  Require ldap-group cn=dev,ou=Group,dc=example,dc=com
-  Require valid-user
+  SSLVerifyClient require
+  SSLVerifyDepth 1
+  SSLUserName SSL_CLIENT_S_DN_CN
   Order allow,deny
   Allow from all
 </Directory>

The clone should work without you being prompted for passwords or anything, showing that SSL client certificate authentication is working. Note as in all other examples, the gitweb configuration was also changed. This means that you must load certificates into your web browser in order to view the content. In the example above, you can install the file "/etc/CA/archive/user6@example.com/client.p12", which has the static password "mypass", into your browser. See Fermilab for some hints on how to do this in different browsers.

Restricting client certificates by group

Again, the user we tested with was part of group sales (gotta watch those Sales 'droids every minute!). So we can restrict access to the git repository to only users with the organizationalunit SCM.


sed -i 's:\( *\)\(SSLUserName.*\):\1\2\n\1SSLRequire ( %{SSL_CLIENT_S_DN_OU} eq "SCM" ):' /etc/httpd/conf.d/git.conf
service httpd reload
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

   SSLVerifyClient require
   SSLVerifyDepth 1
   SSLUserName SSL_CLIENT_S_DN_CN
+  SSLRequire ( %{SSL_CLIENT_S_DN_OU} eq "SCM" )
   AddHandler cgi-script .cgi
   DirectoryIndex gitweb.cgi
 </Directory>
   SSLVerifyClient require
   SSLVerifyDepth 1
   SSLUserName SSL_CLIENT_S_DN_CN
+  SSLRequire ( %{SSL_CLIENT_S_DN_OU} eq "SCM" )
   Order allow,deny
   Allow from all
 </Directory>

That should have failed because we were still using the sales ssl certificate. Now we can try as a different user:


cp /etc/CA/archive/user7@example.com/client.crt ~/.ca/user7.crt
cp /etc/CA/archive/user7@example.com/client.key ~/.ca/user7.key
git config --global http.sslCert ~/.ca/user7.crt
git config --global http.sslKey ~/.ca/user7.key
rm -rf GitBestForks
git clone https://`hostname`/G/GitBestForks

That should work since user7 is in the correct group. Note that gitweb will not work until you delete the user6 certificate from your browser, and add the user7 certificate.

Limited Cleanup

Please remember that if you followed the instructions above, we disabled iptables, added a system account user, created a certificate authority, created some test directories, installed new httpd certificates, and installed a new /etc/gitweb.conf.


service iptables start
userdel -rf user3
rm -ri /etc/CA /src ~/GOTW ~/.ca /etc/pam.d/http
[ -f $HTTPS_CERT.orig ] && ( mv $HTTPS_CERT.orig $HTTPS_CERT; mv $HTTPS_KEY.orig $HTTPS_KEY )
[ -f /etc/gitweb.conf.dist ] && mv /etc/gitweb.conf.dist /etc/gitweb.conf

Reverting other configuration changes (such as our starting httpd, installing git (in two places!), gitweb, LDAP and sometimes other software, configuring LDAP, loading LDAP accounts, modifying /etc/httpd/conf.d/git.conf, modifying SELinux policy, installing mod_authn_sasl) is left as an exercise for the reader.

Disclaimer

Information is not promised or guaranteed to be correct, current, or complete, and may be out of date and may contain technical inaccuracies or typographical errors. Any reliance on this material is at your own risk. No one assumes any responsibility (and everyone expressly disclaims responsibility) for updates to keep information current or to ensure the accuracy or completeness of any posted information. Accordingly, you should confirm the accuracy and completeness of all posted information before making any decision related to any and all matters described.

Thanks

Thanks to the experts on #git and my coworkers for review, feedback, and ideas.

Comments

Comments and improvements welcome.

Use the github issue tracker or discuss with SethRobertson (and others) on #git