Juggling multiple Git accounts

This blog post explains how you can have multiple Github/Bitbucket accounts and get connectivity over SSH to work properly.

January 15, 2020
productivity hacks

In a previous blog I explained how to have different Git identities (e.g. work and private) with different settings, and making that work automatically. I’ve used that setup ever since, and it works very well. Until you run into that edge case, where you need to have multiple Git accounts..

The problem

I have private Github and Bitbucket accounts, and until recently whenever a client or employer used these services they would usually just add my existing account to their team/organization and grant me access that way. That recently changed when I had to create a second Bitbucket account for a project I was working on. While it makes total sense for a company to want to leverage SSO, or to not want all kinds of personal email addresses in their Git history (hello GDPR!), it turned out to be a bit inconvenient for me.

I prefer to use git-over-SSH for communicating with Bitbucket. It’s easy to set up, reliable, and it works well. All you need is your SSH key. Until you have 2 accounts with the same service (e.g. Bitbucket), since it means you need 2 different SSH keys. And then, how do you configure SSH to use the right key for bitbucket.org depending on the repository?

Situation-specific SSH?

I use a lot of situation-specific config and direnv to automatically configure my shell depending on which directory I’m in. That way I make sure to always use the correct AWS config/credentials files, only load credentials/profiles when actually needed (and unload them after), point to different Kubeconfigs, etcetera. It makes switching between different projects, different AWS accounts or different Kubernetes clusters a breeze (just cd <somedirectory>). Surely there must be a way to do something similar for SSH config?

My first, unsuccessful attempt was to try and make SSH use a different config file. The single ~/.ssh/config file has annoyed me for years, but this time it actually blocked me so I was motivated to find a solution. Unfortunately, there’s nothing like configuring $SSH_CONFIG_FILE or adding an [includeIf "<somedirectory>"] like Git allows. So that didn’t really lead anywhere.

The second approach I tried was to configure multiple SSH agents, and use direnv to configure $SSH_AUTH_SOCK depending on where I was to use the proper agent. It’s a decent approach, but managing several SSH agents while still having to think about which keys cannot be in the same agent just wasn’t bomb-proof enough for me. Also, this approach would make forwarding keys a lot more tedious, and also cause serious issues when working with tmux (especially when reattaching sessions). So that didn’t lead anywhere either.

It seemed like the ‘best’ approach on offer was to create an alias in my SSH config file, like:

Host personal-bitbucket
  Hostname bitbucket.org
  User git
  Port 22
  IdentityFile ~/.ssh/personal-bitbucket
  IdentitiesOnly yes

Host work-bitbucket
  Hostname bitbucket.org
  User git
  Port 22
  IdentityFile ~/.ssh/work-bitbucket
  IdentitiesOnly yes

Then, using this SSH config I could set my remotes to be work-bitbucket:workorg/somerepo.git or personal-bitbucket:myname/otherrepo.git and SSH would figure it out. Great! Except that this breaks when a work repo uses another work repo as a submodule. Surely that submodule isn’t set to work-bitbucket:workorg/blah.git but to git@bitbucket.org:workorg/blah.git.. and suddenly everything breaks. The same issue will happen when using Terraform with modules that are sourced from a git repo.

A better solution

So, how do I set up SSH so that I can use the actual hostname bitbucket.org for my Git remotes, submodules, Terraform module sources and whatnot, and still make SSH use the correct SSH key?

The answer lies in a somewhat obscure piece of OpenSSH functionality called Match. You can use Match in your SSH config file (e.g. ~/.ssh/config) to configure some settings based on a filter match. It can filter on hostnames, remote user, local user, or run arbitrary commands. So you can configure a setting based on an arbitrary command returning zero? That’s all we need!

I tend to organize my Git repositories based on ownership. So my ~/git tree looks a bit like this:

git
├── archive
├── employer
├── clientA
├── clientB
├── dotfiles
└── personal

Based on this, a good assumption would be that for everything inside ~/git/clientA I want to use my ‘clientA’ Bitbucket account (and therefore SSH key). For everything outside that directory, SSH can default to using my personal Bitbucket account. To set this up, you can add something like this inside your SSH config:

Host bitbucket.org
Port 22
User git
IdentitiesOnly yes
Match exec "pwd | grep 'git/clientA' &>/dev/null"
IdentityFile ~/.ssh/clientA
Match exec "pwd | grep -v 'git/clientA' &>/dev/null"
IdentityFile ~/.ssh/personal

If I’m running Git (or SSH) from a directory that matches git/clientA it uses the ‘clientA’ key. If my current working directory does not match, it uses my personal key. And obviously I can scale this to even more Bitbucket accounts if necessary.

Tips

  • Make sure to set IdentitiesOnly if you have both keys in your SSH agent, otherwise connections may still fail. The IdentitiesOnly setting will make sure that only the selected key is used, instead of every key in your running agent.
  • You can use the same approach to configure any setting. For instance, you may need ProxyCommand on some corporate networks. Or you may need to set the Hostname field to a different value depending on whether you’re at the office or working frome home.
  • You can also use Match to configure ‘global’ settings (outside of a Host block).