Hi there.
I recently had the urge to migrate our Gerrit installation away from using github OAUTH and chose to use HTTP client certificates instead. Since this required a bit more thinking (and bugging people on IRC) than I'd expected, I would like to share my experience with the community; maybe somebody finds this useful.
Feel free to skip to the bottom if you're the impatient type.
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 HTTPS termination.
Client certificates
We started by setting up a custom CA and issuing a bunch of client certificates. I won't go into too much detail, this is pretty straight-forward. To allow Gerrit to match the certificate to a user, we put the Gerrit user ID in the certificates' Common Name (CN) field. This was wrong, but quite useful for debugging later on.
Nginx and Gerrit
In order to keep things simple, we wanted nginx to validate the client certificates, extract the user ID from the CN field and pass it to Gerrit. So we enabled certificate authentication:
ssl_client_certificate /etc/ssl/certs/http-client-cacert.pem;
ssl_verify_client on;
Then, we added a custom header field to pass the CN to Gerrit:
location / {
...
proxy_set_header CertCN $ssl_client_s_dn_cn;
}
Which needs this little helper:
map $ssl_client_s_dn $ssl_client_s_dn_cn {
default "";
~,CN=(?<CN>[^,]+) $CN;
}
And finally we changed Gerrit's configuration:
[auth]
type = HTTP
httpHeader = CertCN
So far, so simple. Unfortunately it didn't quite work.
Matching IDs
To be more precise, it did actually work. I could connect to Gerrit, nginx would ask me for the certificate, but when I logged in I was not using my own account, instead I was using a fresh account with my Gerrit user ID (1000000) as the username.
So, apparently Gerrit wanted to use the value from the CN fields as the user name, not the user ID. No problem, I made myself a new certificate with myname in the CN field. This made things a bit worse: When trying to log in, I got this type of error in the logs instead:
com.google.gerrit.server.account.AccountException: Cannot assign external ID "username:myname" to account 1000030; external ID already in use.
Clearly, Gerrit was reading the information from the CN field just fine, but instead of using it to authenticate my user ("myname"), it kept creating new users and trying to associate my existing username with them. This is where I ultimately jumped on IRC to get some help.
As it turns out, (I'm sure most of you already know this) Gerrit uses a special git repo to
manage its users. Since it had already created a new user with the name
1000000 during my previous login, at this point it made sense to get a copy of that repo and have a look at what exactly Gerrit was doing / trying to do:
git fetch origin refs/meta/external-ids:external-ids
Which is what the docs and various people on the Internet told me to do. Unfortunately I got an error message, saying that there was no such thing as refs/meta/external-ids on the remote repo. As it turns out, that message was a bit misleading -- I just didn't have the right permissions. Granting myself "Access Database" permissions fixed that (of course I had to undo the configuration changes first to log in).
Now I was able to fetch external-ids and look into the issue. What I saw was that the most recent commit said:
Create Account on First Login
Which happened right around the time when I was first trying to use certificate authentication. Bingo. Looking at the commit with git show, it created two new files. The first one saying:
[externalId "gerrit:1000000"]
accountId = 1000029
And the second one:
[externalId "username:1000000"]
accountId = 1000029
Which explained the error message I was getting: Gerrit uses these files to map identities (such as external IDs, but also Gerrit usernames) to account IDs. Creating a new account means that two files are added to the repo -- one for the external ID (in this case "gerrit:1000000", where 1000000 was the value from the CN field) and one for the username.
As I was trying to log in with my new certificate, using "myname" as its CN, Gerrit tried to find a mapping for "gerrit:myname", found none and proceeded to create new user. Then, as it tried assign the username "myname" to that new account, it noticed that there was already such a user and gave up.
Since it was now quite clear how Gerrit matches the HTTP authenticated user names to its accountId, we were able to add such as mapping ourselves:
[externalId "gerrit:myname"]
accountId = 1000000
$ echo -n "gerrit:myname" | sha1sum
d919131386679458b4d7a5f47dc23164616664ac -
So we put the mapping in a file called d919131386679458b4d7a5f47dc23164616664ac, added it to the repo, committed and pushed the change:
$ git push origin HEAD:refs/meta/external-ids
This last step needs Push access on refs/meta/external-ids.
And that's it. We can now use our little CA to create client certificates to authenticate Gerrit users. Wonderful.
Thank you for listening and thanks to Paul Fertser on IRC for pointing me in the right direction.
TL;DR
- Configure Nginx to use client certificate authentication and to forward the CN to Gerrit
- Configure Gerrit to use HTTP authentication
- Create client certificate with your Gerrit username in the CN field
- Add a mapping for gerrit:yourname to All-Users repo
- Profit