EXPEDIA GROUP TECHNOLOGY — ENGINEERING

How to Handle Multiple Git Accounts

The easy way to get your Gits together

Russell Brown
Expedia Group Technology

--

When you need to work with multiple Git identities, use Git’s ssh-command configuration option to control both your SSH credentials and your Git identity with a single configuration based on your directory path.

If that sentence triggered your eureka moment, stop reading and go create something wonderful. If you want to learn more about how and why, read on.

Decorative cartoon of cat with octopus tentacles surrounded by many filing cabinets
Image generated with OpenAI’s DALL•E 2

A grove of Gits doesn’t make you weird

Working for a bring-your-own-device (BYOD) company is an obvious reason one might use multiple Git accounts, but there are plenty of others. For example, if your company participates in open source, you might have an account in your company’s GitHub Enterprise for internal work product and another in the public GitHub.com for your open source contributions. Or you may have a dedicated GitHub.com account you use for contributions to your employer’s open source products separate from the one you’d use to contribute to other open source projects. Or perhaps you are self-employed and use a different GitHub.com account for each of your ventures.

Managing these multiple configurations efficiently is painful without a carefully-considered solution.

Why multiple Git accounts are hard to manage

The short answer to this question is that two things need to be managed in most Git usages:

  • Identity: Information about you attached to a commit inside a repo. This is managed by the Git application.
  • Authentication: Verifying that you are in fact you before you are allowed to interact with a repo. This is managed by your local computer and the service hosting your remote repos.

These two mechanisms are configured separately. They both support some conditionality in their configuration files, but they don’t consider the same factors, which leads to maintaining two parallel sets of configuration if not configured cleverly.

Your identity in Git

When you use Git locally on your computer, you have elements of identity that you configure locally that are used to mark your commits, common examples of which are:

  • User name
  • Email address
  • GPG signing key

You do use a GPG signing key, right? You should.

These are managed by Git itself in a config file, usually ~/.gitconfig. Here’s a pretty typical example that contains the identity fields above as well as some global preferences:

[merge]
ff = false
[pull]
ff = only
[core]
editor = vim
[commit]
gpgsign = true
[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F

The user name and email address are then consumed from that file and placed into the commits you make on your local repo. Git does not validate or constrain these values, which opens the door to “commit spoofing”. Using a GPG signing key allows a GitHub hosting service to ensure the identity included in the commit is legitimate. This is important in public repositories and especially in open source projects.

Conditional configuration

Your .gitconfig doesn't need to be your only configuration for Git. That file's syntax supports including additional files containing Git configuration with include. In addition, you can make it conditional using the includeIf directive. Its documentation lays out a couple of options, but the one that is most interesting for this use case is gitdir.

If you commit (tee hee) to storing repos in directories that correspond to identities, you can use includeIf to set your identity in the repos below them. I use a structure similar to:

$HOME
+- git
+- work
+- oss
+- personal

Then my .gitconfig looks like this:

[merge]
ff = false
[pull]
ff = only
[core]
editor = vim

# The trailing slashes below are required to match
# any subdirectory and not just the exact path

[includeIf "gitdir:~/git/work/"]
path = ~/.gitconfig.work
[includeIf "gitdir:~/git/oss/"]
path = ~/.gitconfig.oss
[includeIf "gitdir:~/git/personal/"]
path = ~/.gitconfig.personal

The .gitconfig.work configuration file looks like this (the others are similar with different emails and signing keys):

[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true

Whenever I invoke Git from any subdirectory of ~/git/work (that's what the trailing slash does in the gitdir spec), this will be the effective configuration:

[merge]
ff = false
[pull]
ff = only
[core]
editor = vim
[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true

Which solves the problem of including the proper name, email, and signing key for commits to my work repos.

Diagram illustrating the relationship between config files and Git settings as described in the text and examples
How directory can be mapped to .gitconfig files per identity and used in the git command

This solves the identity management problem. It does not account for authenticating to multiple Git hosts, however.

Hosted Git

In most professional settings, Git’s server is used behind a hosting platform such as GitHub (or GitHub Enterprise), GitLab, or BitBucket. These hosting platforms provide authentication, role-based access control (RBAC), organizational structures, issues, and collaboration features like pull requests. I’m most familiar with GitHub and it is the market leader, so I’ll focus on that product, but the other products have similar capabilities.

Authenticating to GitHub

Because GitHub in all its flavors is designed to be a central authority for Git repos that are shared among developers, but local to none, users must be authenticated to have access to the repos it hosts. You can configure a local Git client to use HTTPS to interact with repos hosted in GitHub, but Git has no ability to store credentials and no daemon to cache them — so you’ll need to log in for each clone, push, and pull.

To work around this, Git can use the operating system’s secure shell (SSH) facility to authenticate you and encrypt your communications with a remote. This works by using an RSA key pair:

  • The secret key is stored on your computer
  • Its corresponding public key is stored in the remote

Because the remote is managed by a GitHub server, you enter the text of the public key using the GitHub UI; you don’t have a config file for this. When you push, pull, or clone, your connection to the remote is crafted with the private key and verified by the remote using the public key.

Your private key is stored in the file ~/.ssh/id_rsa by default. The paired public key is stored under the same name and place as the private key, with .pub appended to its name. SSH will use these names unless you specify otherwise when you use them.

You can make multiple keys if you supply different names for each.

Choosing an SSH key

SSH keeps a config file, ~/.ssh/config by default, that allows it to choose which key to use for a particular host as well as some other configuration options. Here's an example of what goes in it:

Host personal
Hostname github.com
User rcbrown
Host *
IdentityFile ~/.ssh/id_rsa

That Hostname is picked out of the Git URL used when cloning:

git@github.com:rcbrown/grpc-node-examples.git

It’s the portion between @ and :. The Host * configurations will be added to the matching host's entry, or used exclusively if nothing else matches. In this case, the effective configuration of SSH will be:

Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa

If you were to use SSH with two different GitHub servers, your SSH config might look something like this:

Host work
Hostname github.myemployer.com
User rbrown
Host personal
Hostname github.com
User rcbrown
Host *
IdentityFile ~/.ssh/id_rsa

Using the same SSH public key on multiple accounts is poor practice — so much so that GitHub prevents it. You will need two different SSH keypairs. You can handle that by moving the IdentityFile setting out of the common block and into host-specific ones:

Host work
Hostname github.myemployer.com
User rbrown
IdentityFile ~/.ssh/id_rsa_work
Host personal
Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa_personal
Host *
IdentityFile ~/.ssh/id_rsa

What happens when you have two accounts on the same server? For example, a GitHub.com for your open source contributions, and another for your personal stuff? This is where it gets interesting. Setting up the config file is easy enough:

Host work
Hostname github.myemployer.com
User rbrown
IdentityFile ~/.ssh/id_rsa_work
Host personal
Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa_personal
Host oss
Hostname github.com
User open_rcbrown
IdentityFile ~/.ssh/id_rsa_oss
Host *
IdentityFile ~/.ssh/id_rsa

But how do you decide whether to use personal or oss when accessing Github.com?

A red herring

Put “multiple GitHub accounts” in your favorite search engine and the first few results will probably propose creating the .ssh/config file as I showed above, then modifying the hostname to choose the correct key when Git interacts with SSH:

git clone git@personal:rcbrown/grpc-node-examples.git

In my opinion, this solution has serious drawbacks.

  • GitHub makes it easy to copy the clone URL to the clipboard, but you will have to modify the hostname in the command line that you must actually run. This is easy to forget. It’s even more annoying if you are forking a repo and maintaining an upstream.
  • It’s a hierarchy parallel to your GitHub identity; adding a new account requires updating both .gitconfig and ~/.ssh/config.
  • Building on the last point, because separate directories are required to vary the Git identity configuration, the SSH server name must be set in a hostname that will be used only under the directory that provides the correct identity for that host. In other words, for this to work right, every repo needs both to have the right name and to be in the right place.

The fragmented config is difficult to manage:

Diagram illustrating the relationship between config files and Git settings as described in the text and examples
Using directory to supply identity to git while using repo hostname to find the correct SSH key

What we need to solve these problems is a way to unify the identity and authentication configurations. To do that, we need to find a decision point that intersects both Git identity and SSH host identity.

Rescued by sshCommand

The Git config files that you include from ifInclude directives in .gitconfig that control identity by directory can control more. You can take the choice of SSH key away from SSH and let Git choose the key and the identity using the includeIf mechanism.

  • You can tell Git what SSH command to use in a config file by using the sshCommand setting.
  • Since you know how to tell Git to use specific configs per directory, you can configure an SSH command per directory.
  • You can construct an SSH command that uses a specific identity with the -i option.

Here’s what .gitconfig.work would look like:

[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true
[core]
sshCommand = "ssh -i ~/.ssh/id_rsa_work

Therefore, you can control both Git identity and SSH authentication using .gitconfig and appropriate directories. No need for strange Git hostnames. A side effect is that the usual SSH identity search rules (described in SSH's man page) are no longer relevant because the identity to use is always explicit — less to reason about, a good thing in my opinion.

Diagram illustrating the relationship between config files and Git settings as described in the text and examples
Unified configuration that determines both identity and SSH key from directory

Alternatives

There are other ways to do this. Aside from using modified hostnames, which I rejected above, you can include a repo-level Git config in <repo>/.git/config. This configuration file could be a symbolic link to one of the environment-specific Git configs we created above (such as ~/.gitconfig.work). You will have to remember to create the link in each repo you clone, but once you've done so, you don't have to touch it again. This solution makes the most sense if you're only using an SSH key for one repo or if you have a large number of Git hosts to work with and relatively few repos in each.

Do you have more ideas? I’d love to hear them!

Learn more about technology at Expedia Group

--

--