reynir.dk/posts/2015-11-08-14-10-SSH%20certificates%20with%20Gitolite.markdown
Reynir Björnsson c0854750d3 Fix some things, urlencode post names
the jinja2 function urlencode is apparently not implemented :(
2023-11-14 11:35:11 +01:00

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