Migrating Gerrit to HTTP certificate authentication

We have been running an installation of the Gerrit code review software for a while, using GitHub OAUTH to authenticate our users. However, having a third party in control of who can use your infrastructure and letting them decide how we are allowed to authenticate has been bugging me. So it was time to take things into our own hands and we decided to use HTTP client certificates for authentication instead.

Admittedly, the fact that GitHub recently decided to enforce their view on secure authentication combined with them being owned by an entity that is commonly known to be "more evil than satan himself" may have ever so slightly boosted my motivation to look into this.

Anyway. This is what I came up with after seeking advice on IRC; may it prove useful to others and/or future versions of myself.

Initial state of things

As mentioned, we had been using OAUTH with GitHub for a while, our users' primary identity was github-oauth:id (with id being a number). The Gerrit installation was already running behind an Nginx reverse-proxy that was dealing with things such as HTTPS termination.

Preparing Gerrit for the migration

To follow the steps listed below, you will need to grant yourself some non-standard permissions on Gerrit:

  • Access Database for All-Projects
  • Push to refs/meta/external-ids for All-Users

To make your life a bit easier, I would recommend adding these permissions before you start messing with authentication.

Client certificates

To authenticate our users' certificates, they need to be signed by a custom certificate authority (CA). So we created a fresh one. I won't go into too much detail, this is pretty straight-forward and others have explained the steps in great detail.

For us, the important part is the Common Name (CN) field of our certificates. This field has to match a user's Gerrit username:

Use this value when creating your client certificate:

Common Name (e.g. server FQDN or YOUR name) []: myname

This allows Gerrit to match the certificate to your user.

Nginx and Gerrit

Nginx' job is to check that the client is supplying a certificate, that the certificate is valid and that it has been signed by our CA. The following lines in your site configuration will take care of that:

ssl_client_certificate /etc/ssl/certs/http-client-cacert.pem;
ssl_verify_client on;

Now that we know the client is using a valid, signed certificate, we should pass the Common Name (CN) field from the certificate to Gerrit. We can tell Nginx to add a custom HTTP header to all traffic sent to our backend:

location / {
    proxy_set_header CertCN $ssl_client_s_dn_cn;

This needs this little helper:

map $ssl_client_s_dn $ssl_client_s_dn_cn {
    default "";
    ~,CN=(?[^,]+) $CN;

And finally we configure Gerrit to use HTTP authentication and to get the user name from our custom header:

    type = HTTP
    httpHeader = CertCN

So far, so simple. Now for the hairy part.

Matching IDs

At the moment there is one piece missing: While we do pass our username from the certificate to Gerrit, Gerrit doesn't just believe us, it needs to have an explicit mapping from the HTTP username to your account ID defined. This section explains how to figure out the details; if you don't care, free free to skip ahead.

If you try to login with the current configuration, you will get an error message and the logs will say something like this:

com.google.gerrit.server.account.AccountException: Cannot assign external ID "username:myname" to account 1000030; external ID already in use.

This happens due to the way Gerrit manages its users. The docs explain this in great detail, yet it took me a while to understand it (actually I had to ask on IRC). So let's get our hands on the user database and investigate!

The users are kept in a special repository called All-Users. We can just clone that like any other repository:

$ git clone ssh://myname@review.example.org:29418/All-Users
$ git fetch origin refs/meta/external-ids:external-ids
$ git checkout external-ids

I got stuck at step 2:

$ git fetch origin refs/meta/external-ids:external-ids
fatal: couldn't find remote ref refs/meta/external-ids

As it turns out, that happens if you don't have the Access Database permissions. If you do have the right permissions, you should now have a folder containing a bunch of files that use SHA-1 sums for their names:

$ ls
03bf4ca507c84391db303cf11ce40e332662576a  a24d64867c0ba5436f14c68afada8ab1a7543fac
2aea47860ebe0d72e3ae71cf4bb38583815ca4d7  acee4a74a1fe82b0e7fcabf7338a0131ef9de1cd
2e28e931f0a13f24c34e8bc1dc953dece74ff283  b01805d3be5b980c714ae929e32e17664bf2d929

Each of these files contains a mapping from some external ID to a Gerrit user. You can find all your mappings via your account ID:

$ grep -C 2 -h 'accountId = 1000000' *
[externalId "username:myname"]
	accountId = 1000000
	password = bcrypt0:4:2Km6lKyA0LaIdTXrQuAlSw==:h/MijKybRny9jS+MPo4218cf2e/yCL74
[externalId "mailto:me@example.org"]
	accountId = 1000000
	email = me@example.org
[externalId "github-oauth:12345678"]
	accountId = 1000000

This explains the error message we got: Gerrit tried to create a new user with the ID 1000030 and assign our existing username (username:myname) to it. Luckily that didn't work. So how can we tell Gerrit to use our existing user instead of creating a new one?

To figure this out, I created a new client certificate for myself with a different user name and used that certificate to login. Gerrit created a new user in the All-Users repository for me with these mappings:

$ grep -C 2 -h 'accountId = 1000029' *
[externalId "gerrit:newname"]
	accountId = 1000029
[externalId "username:newname"]
	accountId = 1000029

Bingo! This new user has two external IDs assigned to it: The username:newname ID is similar to the one we had before, the gerrit:newname ID is new. So we should probably add a similar new ID to our existing user.

Adding a new ID

Now we need to map gerrit:myname to accountId = 1000000. So let's create a file to do just that:

[externalId "gerrit:myname"]
	accountId = 1000000

The file name, as described in the docs, is simply a hash of the external ID:

$ echo -n "gerrit:myname" | sha1sum
d919131386679458b4d7a5f47dc23164616664ac  -

So we put the mapping in a file called d919131386679458b4d7a5f47dc23164616664ac, add it to the repo, commit and push the change:

$ git push origin HEAD:refs/meta/external-ids

This last step needs Push permissions on refs/meta/external-ids.

And that's it. Gerrit should now properly map the HTTP authenticated user to our user ID and we can use our little CA to create client certificates for all our Gerrit users. Wonderful.

Thank you for listening and thanks to Paul Fertser for pointing me in the right direction on IRC.