Whilst ramping up on a client project this year, the client’s security team required myself and our team to have separate GitHub accounts for access to any of the client’s GitHub repositories. Setting up multiple Git accounts on one user account on my Mac took quite a few iterations to get right and felt worth sharing.
The separate GitHub account restriction had several issues with it to be solved:
- Both K. Adam White (my teammate on this project) and I contribute to Human Made repositories regularly and would still need access to push and pull private repos from our HM GH accounts.
- We both wanted to maintain our normal tooling and workflow setup, i.e. not just create another account on a Mac.
- GitHub doesn’t allow you to re-use SSH keys on separate accounts, so we had to create new Git configs and new SSH keys. Additionally, due to a restriction in the way Docker accesses SSH keys we had to create keys without passphrases to install private Git repos via Composer and NPM within the containers.
- Related to the above, we’d need new SSH keys, but would only want to use those SSH keys when interfacing with the client’s repos. We’d need our original SSH and GPG keys maintained for our normal GH accounts.
- The client’s local dev environment scripting relied on using the default
~/.ssh/id_rsa
key location without the option to set a different key for those commands.
All of the above led to the need to maintain two separate sets of GitHub configurations and SSH keys, depending on which repository we were working with.
The Best Option: Switch all Configuration Files
K. Adam eventually came up with the below solution which we’ve stuck with. We have setup individual SSH keys and Git configs for our origin (HM) setup and for our client setup. We then use the following Bash functions to switch back and forth.
########################################
Switch to using client-specific SSH key and .gitconfig.
# Arguments:
# None
# Returns:
# None
#######################################
client-ssh-enable() {
# Copy the client gitconfig to default.
cp ~/.gitconfig_client ~/.gitconfig
# Copy the client key to the default name.
cp ~/.ssh/client_rsa ~/.ssh/id_rsa
cp ~/.ssh/client_rsa.pub ~/.ssh/id_rsa.pub
# Setup the client key within the agent.
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
}
#######################################
# Switch to using Human Made-specific SSH key and .gitconfig.
# Arguments:
# None
# Returns:
# None
#######################################
client-ssh-disable() {
# Copy original gitconfig to default.
cp ~/.gitconfig_original ~/.gitconfig
# Copy back the original key.
cp ~/.ssh/original_rsa ~/.ssh/id_rsa
cp ~/.ssh/original_rsa.pub ~/.ssh/id_rsa.pub
# Setup the original key within the agent.
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
}
These functions reside in .bash_profile
and can be called at any time to switch out configurations. We maintain a named Git config file and set of SSH keys for both our original (Human Made) work and our client work and when the correct Bash function is called it will switch to the desired set of configurations.
You’ll note that this method involves maintaining 3 sets of configurations at all times:
- The one currently being used (a copy of one of the other two configs)
- One config named explicitly for the client
- One config named explicitly as the original version.
It is very important to note that you must setup a copy of your original SSH key and .gitconfig
before running this. If you don’t, you will lose your originals. Additionally you’ll need to remember your original keys passphrase, which you likely setup 4 years ago 😄
Other Options
We also had several failed attempts before reaching the above solution, both of them based on restricting all of the client’s repos to a particular directory on our computer, This failed on two fronts:
- Our client has a repository setup and tooling that expects certain Git repos of theirs to reside in the main user account, outside of our designated client directory. The client’s setup explicitly states that you should only setup from a freshly-wiped computer and therefore makes a lot of assumptions about the workspace.
- Composer and NPM both failed when pulling in private repositories as dependencies. Our guess is that both pull in the repos in a temporary directory outside of the
cwd
, although we haven’t confirmed this guess.
I wanted to include these failed options because they might work for some else’s situation.
Brute-force directory option
K. Adam wrote this up in much greater detail so I won’t waste your time with a subpar explanation here. Here’s the short of the code for prettiness:
#!/bin/bash
# When running `git`, if we are within the client project directory, set Git
# to use a specific SSH private key; otherwise act as normal.
if [[ $PWD == /hm/client/* ]]; then
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa_client" /usr/bin/git $@
else
/usr/bin/git $@
fi
Config-only option
I was pretty determined to have a config-only option as I wanted one smart configuration setup that I then didn’t have to think about again. It was more complicated than the above setup, but it should have worked with the inbuilt Git and SSH configuration better.
This had 3 components:
- A check that loaded a separate Git configuration file from my main Git configuration file.
- A string replacement to append a unique identifier for SSH to find
git@github.com
→git@github.com-client
- An SSH configuration that only ran when it found the
git@github.com-client
string that loaded in my particular SSH file.
Main gitconfig:
... {normal gitconfig stuff}
; Override all values if within the "/client/**"
folder[includeIf "gitdir:~/client/"]
path = /Users/mikeselander/.gitconfig_client
Git offers the [includeIf "gitdir:~/directory/"]
check for situations like this where you’d want to modify default behavior based on either directory or branchname. I highly recommend reading the documentation on it.
Client gitconfig:
[user]
email = hm-mikeselander@client.com
name = Mike Selander[github]
user = mikeselander-client
[url "git@github.com-client"]
insteadOf = git@github.com
The important part of this file is the URL replacement – this is to provide a unique string that SSH can hook into in my SSH file as seen below. Note that without the below SSH config modification this would cause Git to reach out to a non-existent host.
SSH config:
... {normal SSh config stuff}
# Client GH accounts
Host github.com-client
HostName github.com
User git
IdentityFile ~/.ssh/client_rsa
Finally, this file looks for any requests over SSH with our particular host of github.com-client
, uses the client-specific SSH key and replaces the host with the valid GitHub one and uses our client-specific SSH key.