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..
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?
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
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
email@example.com: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.
- Make sure to set
IdentitiesOnlyif you have both keys in your SSH agent, otherwise connections may still fail. The
IdentitiesOnlysetting 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
ProxyCommandon some corporate networks. Or you may need to set the
Hostnamefield to a different value depending on whether you’re at the office or working frome home.
- You can also use
Matchto configure ‘global’ settings (outside of a