157 lines
6.3 KiB
Markdown
157 lines
6.3 KiB
Markdown
---
|
|
title: SSH Certificates with Gitolite
|
|
date: 2015-11-08
|
|
---
|
|
|
|
[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](#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:
|
|
|
|
- The first option, `command="/path/to/gitolite-shell rbj"`, is what makes gitolite work with ssh.
|
|
What this option does is whenever I log in as the gitolite user using the corresponding private key the gitolite-shell command will be run instead of the original command or a login shell.
|
|
More on this later.
|
|
|
|
- The other options are all of the form `no-OPTION`.
|
|
These options further restrict what you can do with the ssh key,
|
|
for example it disallows the user from forwarding ports or transfering files with sftp.
|
|
|
|
### 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.](/images/did-you-mean-95f89861f324e4d9768d2a7393f6f8e2.png)
|
|
|
|
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
|
|
|
|
- Passing `-n git` only allows the certificate to be used for logging in as the `git` unix user.
|
|
|
|
- The `-O clear` option unsets all options, and is equivalent to a series of `no-FOO` in authorized\_keys.
|
|
|
|
- The `-O force-command` option does exactly the same as `command="[...]"` in authorized\_keys.
|
|
|
|
An example
|
|
|
|
``` bash
|
|
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
|
|
|
|
``` {#script .bash}
|
|
#!/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}"
|
|
```
|
|
|
|
[1]: https://en.wikipedia.org/wiki/Git_(software)#Git_server
|
|
[Gitolite]: http://gitolite.com/
|
|
[OpenSSH certificates]: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man1/ssh-keygen.1?query=ssh-keygen&sec=1#x434552544946494341544553
|
|
[CERTIFICATES]: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man1/ssh-keygen.1?query=ssh-keygen&sec=1#x434552544946494341544553
|