SSH Certificates with Gitolite

Posted on November 8, 2015

Gitolite is a “an access control layer on top of Git, providing fine access control to Git repositories.” It allows you to host several repositories for several Gitolite users using a single unix user.

A typical setup uses ssh for authentication. A new user is added by commiting a public key in keydir/ to the gitolite-admin repository. The gitolite username corresponding to the public key is determined by the filename of the public key commited to gitolite-admin. For example, keydir/host1.example.com/puppet-reader.pub corresponds to the gitolite user puppet-reader. The directory host1.example.com doesn’t carry any semantics, and is useful for when a user has more than one SSH key.

This post is about how to add new ssh keys for a user without having to push the new public key to the gitolite-admin repository every time. This can be convenient when automatically generating keys for reading repositories containing e.g. code or configuration. An example script is embedded at the end of the post. First we need to know a little about how Gitolite works with SSH authentication.

How does this work?

When a new key in keydir/ is pushed to the gitolite-admin repository a git hook script is run. This script will read the public key and add it to authorized_keys for the gitolite unix user. The entry will look something like the following:

command="/path/to/gitolite-shell rbj",no-port-forwarding,[...] \
	ssh-rsa AAAAB3N[...] \
	rbj@laptop

That is, it is like a regular authorized_keys entry, but with a certain set of options:

So what does gitolite-shell do?

Well, it does a bunch. It is similar to git-shell, but using gitolite’s access control. But basically it checks the user supplied as first argument and $SSH_ORIGINAL_COMMAND against the permissions configured in conf/gitolite.conf. If the user is allowed then the git commands are performed.

Using certificates

This works great. However, it can be a bit inconvenient to add new keys, especially if you do it repeatedly or want to automate it. This is where OpenSSH certificates come to our rescue.

Searching for ssh certificates is notoriously difficult.

Searching for ‘ssh certificates’ is notoriously difficult.

An OpenSSH certificate is a signed file containing a public key and some more information. The certificate is signed by a good old ssh key known as the certificate authority (CA). When creating and signing a certificate we can specify what user(s) the certificate is valid for, and a number of ssh options. I recommend reading the CERTIFICATES section of the ssh-keygen man page for more information.

The certificate options

An example

ssh-keygen -s /path/to/CA -I test -n git \
	-O clear -O force-command="/remote/path/to/gitolte-shell rbj" \
	id_rsa.pub

Trusting the CA

To trust a CA we simply add the key to authorized_keys with the cert-authority. For example,

cert-authority,no-port-forwarding,[...] ssh-rsa AAAAB3N[...] rbj@laptop

The no-FOO options are redundant if you passed -O clear when signing the key, but is nice to have just in case.

Security caveats

Anyone with the CA key can sign keys that are valid for the git unix user. They cannot get a PTY or start an sftp session, but they will still be able to run any command(!) They can also sign a certificate valid for any gitolite user, of course.

Reading the man page this sentence stood out to me: “If both certificate restrictions and key options are present, the most restrictive union of the two is applied.” I figured I could then restrict the cert-authority to one command (one gitolite user) like this:

cert-authority,command="/path/to/gitolite-shell rbj",no-port-forwarding,... \
	ssh-rsa AAAAB3N... \
	rbj@laptop

Unfortunately, OpenSSH seems to prefer the certificate’s force-command option over the command option from authorized_keys!

update: After writing this post I found the following in the sshd(8) man page. Also note that this command may be superseded by either a sshd_config(5) ForceCommand directive or a command embedded in a certificate.

Conclusion

You have to trust the CA to only sign keys for the gitolite-shell command and for the gitolite users you want.

Sample script

#!/bin/sh

# Modify these constants to your needs
cakey=/root/puppet-ca/puppet_ca
# This file contains a single number as a string
serial_file=/root/puppet-ca/serial
# You probably want to change this
gitolite_user=puppet-reader
# Update this to the correct path to gitolite-shell
# Check ~git/.ssh/authorized_keys if in doubt
gitolite_shell=/srv/git/gitolite/src/gitolite-shell

usage() {
        echo Usage:
	echo "$0 PUBKEY KEYID"
	echo
	echo PUBKEY is the key to be signed
	echo KEYID should be a unique name
}

if [ "$#" -ne 2 ]; then
	usage
        exit 1
fi

pubkey=$1
keyid=$2

# Not the most robust code
serial=$(cat "${serial_file}")
echo $((serial+1)) > "${serial_file}"

# You might want to pass a validity period, e.g. -V +52w
ssh-keygen -s "${cakey}" -I "${keyid}" \
        -n 'git' -O clear \
	-O force-command="${gitolite_shell} ${gitolite_user}" \
        -z "${serial}" "${pubkey}"