Using Multiple SSH Deploy Keys with GitHub

Matthias Pigulla  ·  07. September 2020

GitHub Deploy keys are for a single repository only

On GitHub, deploy keys are SSH keys that can be associated with a single repository and granted read-only or read/write permissions. With deploy keys, you don’t need a particular user account to access the repository. Having the key itself suffices – the key is the authentication and authorization token at the same time. 

To limit exposure in case such a key is lost, GitHub enforces a policy that a particular SSH key can only be used as deployment key for a single repository.

This becomes an issue when you want to work with several Git repositories at a time, using different deploy keys: When ssh has several keys available, it will try each of them in turn. The first known key will be accepted by GitHub servers for the SSH connection itself. But (some of) the subsequent Git commands run over the SSH connection will fail, since the SSH key is not authorized for the particular repository. You will see an error like 

fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.

As long as you’re running git commands one by one, you can use the GIT_SSH  environment variable to pass additional arguments to SSH and select the right key for a particular repository. However, when using package managers like Yarn, NPM or Composer that might need to clone several repositories, this won’t work.

Using virtual GitHub.com subdomains?

In this situation, people have suggested (here, there, elsewhere...) to make up (nonexistent) GitHub.com subdomains and use those in repository URLs. Then, additional ssh  configuration could be used to map these names back to github.com  as hostname while at the same time specifying the SSH key to use.

The downside of this approach is that these made-up domain names become part of your dependency declaration file, essentially forcing everyone to provide the same mapping in their SSH config files to be able to install dependencies.

Use key comments to find matching keys in the SSH agent

Thus, I would like to suggest another approach: Put all keys, including GitHub deployment keys, into the SSH key agent. Then, make Git use a wrapper script around ssh  to select the right key.

We will use SSH key comments to record the name of the repository that a key belongs to. And we will use the fact that  ssh -i  can point to a file with a key's public part, and that this key will be tried first even if all private keys and handled by the SSH agent.

So, let’s create a dedicated deploy key first:

ssh-keygen -C "Deploy key for git@github.com/your-org/your-repo.git" -f keyfile …

Add your usual arguments ( -t ed25519 -a 100 , maybe?) to create a new SSH key in keyfile . The key's public part will be put into keyfile.pub , and the key's comment will be set to contain your repository name. As this is a deploy key, don't set a passphrase.

Setup the deploy key at GitHub.com according to their deploy key documentation. Repeat this for every repository in question, and don't forget to use different keyfiles.

Next, make sure you have the SSH agent running, or start it with eval `ssh-agent -s` . Add all your key files to it by running ssh-add keyfile keyfile2 … . You can use ssh-add -L  to list all keys currently loaded into the agent.

The Wrapper Script

Put the following wrapper script somewhere:

#!/bin/bash

# The last argument is the command to be executed on the remote end, which is something
# like "git-upload-pack 'webfactory/ssh-agent.git'". We need the repo path only, so we
# loop over this last argument to get the last part of if.
for last in ${!#}; do :; done

# Don't use "exec" to run "ssh" below; then the trap won't work.
key_file=$(mktemp -u)
trap "rm -f $key_file" EXIT

eval last=$last

# Try to pick the right key
ssh-add -L | grep --word-regexp --max-count=1 $last > $key_file

ssh -i $key_file "$@"

(I've put this into a Gist, so you can also  wget https://gist.githubusercontent.com/mpdude/e56fcae5bc541b95187fa764aafb5e6d/raw/676626f0f8009d8b47743417dba436483cd929c6/ssh-deploy-key-wrapper.sh && chmod +x ssh-deploy-key-wrapper.sh ).

Export the GIT_SSH  environment variable to point to this wrapper script. Now, whenever Git needs to execute ssh , it will start this script instead.

The script will go through the keys currently loaded into the ssh-agent  and grep  the first one that matches the your-org/your-repo.git  part from the current repository URL. This is where the key comment we set up above comes into play.

The key's public part is then put into a temporary file and passed to the actual ssh  invocation. As a result, this key will be tried first, and the private key part will still be provided by the SSH agent.

Avatar von Matthias Pigulla

Matthias Pigulla

Diplom-Wirtschaftsinformatiker, Geschäftsführer

Der strategische Kopf hinter unseren Softwaresystemen behält bei der Entwicklung und Betreuung komplexer Architekturen den Überblick, kümmert sich um die technische Infrastruktur oder berichtet über seine Erfahrungen und Erkenntnisse auf der Symfony User Group. Und wenn er abends damit fertig ist, entspannt der zweifache Vater entweder auf seiner Yogamatte oder lässt beim Fotografieren die Seele baumeln.