Blog

Effortless GitHub Account Management: Mastering SSH Agents in WSL and Containers

05 Sep, 2023
Xebia Background Header Wave

Recently I wrote an about using the 1Password SSH agent with Windows and VSCode DevContainers. A short time later I was confronted with the problem of working with 2 GitHub accounts at the same time. At this time, GitHub does not allow to use the same SSH key in multiple accounts, so separate keys for each account have to be created.

This article guides you on how to setup your environment to work with 2 different GitHub accounts using the 1Password SSH agent in WSL and VSCode DevContainers.

Setup SSH

First, create the SSH authentication and signing keys for the new GitHub account and make it available through 1Password SSH agent as described in the previous article.

ssh-add -l
256 SHA256:sPS3deWzqfS8H8Iqa/k1lZ/1AFwf5551uwiSyj/TgAo Github Signing (ED25519)
4096 SHA256:QEzxxocaOsX19Hk2tSv/jeev7qrD8eaQK2eYBj7JvD4 Azure (RSA)
256 SHA256:gaFfPRLGjGotBULiUBUYP6/HMDXvnUbxlHjX2zrpNsM Github (ED25519)
256 SHA256:lviYy9axbmWN7NbOz4LsGgHu9jCX6fJVgnhk+lFVGh8 Github Signing Custom (ED25519)
256 SHA256:OO9rRRIrxGkPouVDy1mNYI/m0KZvS+gTtqWuzu4Zg/g Github Custom (ED25519)

Afterwards ensure the SSH agent will use the right key for each repository. This can be achieved using different SSH configs for the same host defining which key to use. Therefore we need to export the public ssh keys as described in the 1Password docs.

~/.ssh/pubkeys$ ll
total 20
drwxr-xr-x 2 marius marius 4096 Sep  4 14:58 ./
drwx------ 4 marius marius 4096 Sep  4 14:38 ../
-rw------- 1 marius marius  724 Aug 21 11:21 azdo.pub
-rw------- 1 marius marius   80 Aug 21 11:20 github.pub
-rw------- 1 marius marius   80 Sep  4 14:36 github_custom.pub

The SSH config allows for configuring a public key as IdentityFile. But as the SSH agent has its own logic on which key to use this will only add the corresponding private keys to the list of keys to consider for SSH authentication with the given server and the agent may still pick the wrong one from the list. To ensure only the given key will be used, enable the setting IdentitiesOnly for the configured host. Now the agent will only consider the private SSH key for the given public key.

Host *
  ForwardAgent yes
  IdentityAgent /tmp/1password-agent.sock

Host github.com
  HostName github.com
  IdentityFile ~/.ssh/pubkeys/github.pub
  IdentitiesOnly yes

Host githubcustom
  HostName github.com
  IdentityFile ~/.ssh/pubkeys/github_custom.pub
  IdentitiesOnly yes

Host ssh.dev.azure.com
  Hostname ssh.dev.azure.com
  IdentityFile ~/.ssh/pubkeys/azdo.pub
  IdentitiesOnly yes

Setup Git

Fine, now we’ve set up different SSH hosts for github.com. Still if we clone a repo from github.com only the first one will be chosen, because the server in the clone-url is always github.com.

Host github.com
  HostName github.com
  IdentityFile ~/.ssh/github.pub
  IdentitiesOnly yes

Host githubcustom
  HostName github.com
  IdentityFile ~/.ssh/github_custom.pub
  IdentitiesOnly yes

So the clone URL

[email protected]:shrutikapoor08/devjoke.git

has to be changed to

git@githubcustom:shrutikapoor08/devjoke.git

To make this more convenient in development workflows we’ll create an git alias for cloning repos in the name of the custom account.

# config alias
git config --global alias.clone-custom '!f() { git clone --config user.email='[email protected]' --config user.signingkey='ssh-ssh-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxhFVyilOVwuMW8AAmtHqemrJDzT7d14VAR4Rcj81u4' $(echo $1 | sed 's/[email protected]/git@githubcustom/g'); }; f'

# clone repo
git clone-custom [email protected]:shrutikapoor08/devjoke.git

The alias starts with ! to execute a complex command in a separate shell. Afterwards we define a function f for cloning and configuring the repo. Finally the function is called with the argument of the alias command.

To make this more readable let’s write the function out.

# define function
f() {
  git clone 
  --config user.email='[email protected]' 
  --config user.signingkey='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxhFVyilOVwuMW8AAmtHqemrJDzT7d14VAR4Rcj81u4' 
  $(echo $1 | sed 's/[email protected]/git@githubcustom/g');
}

# call function
f [email protected]:shrutikapoor08/devjoke.git

As we can see the function f() calls git clone with the --config option to configure the individual email and signing key of the user. The clone url is the functions parameter with github.com replaced by our custom ssh target githubcustom.

Run in WSL

Now we can clone a repo with our default account as usual.

# clone repo
git clone [email protected]:shrutikapoor08/devjoke.git devjokes-default
Cloning into 'devjokes-default'...
remote: Enumerating objects: 2441, done.
remote: Counting objects: 100% (234/234), done.
remote: Compressing objects: 100% (174/174), done.
remote: Total 2441 (delta 137), reused 149 (delta 60), pack-reused 2207
Receiving objects: 100% (2441/2441), 56.87 MiB | 6.24 MiB/s, done.
Resolving deltas: 100% (1251/1251), done.

# switch into repository
cd devjokes-default/

# display local git configuration
git config --local --list
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
[email protected]:shrutikapoor08/devjoke.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

# display git remotes
git remote -v
origin  [email protected]:shrutikapoor08/devjoke.git (fetch)
origin  [email protected]:shrutikapoor08/devjoke.git (push)

As we can see there is no local configuration for the user’s email and signing key so the gobal configuration will be used. Origin points to github.com as it should be for the default account.

But if we use our new aliased command we can clone and configure the repo for working with the custom account.

# clone custom
git clone-custom [email protected]:shrutikapoor08/devjoke.git devjokes-custom
Cloning into 'devjokes-custom'...
remote: Enumerating objects: 2441, done.
remote: Counting objects: 100% (234/234), done.
remote: Compressing objects: 100% (174/174), done.
remote: Total 2441 (delta 137), reused 149 (delta 60), pack-reused 2207
Receiving objects: 100% (2441/2441), 56.87 MiB | 6.49 MiB/s, done.
Resolving deltas: 100% (1251/1251), done.

# switch into repository
cd devjokes-custom/

# display local git configuration
git config --local --list
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
[email protected]
user.signingkey=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxhFVyilOVwuMW8AAmtHqemrJDzT7d14VAR4Rcj81u4
remote.origin.url=git@githubcustom:shrutikapoor08/devjoke.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

# display git remotes
git remote -v
origin  git@githubcustom:shrutikapoor08/devjoke.git (fetch)
origin  git@githubcustom:shrutikapoor08/devjoke.git (push)

The local configuration of the user’s email and signing key will override the global configuration and the origin points to the custom ssh target.

Run in VSCode DevContainer

The devcontainer extension already does a lot to make working with your repositories inside a container as comfortable as possible. For example we do not need to worry on how to get our global git configuration into the container.

The extension will automatically copy your local .gitconfig file into the container on startup so you should not need to do this in the container itself. https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials

This is great as we can work with git within the container the same way we do outside. However in our specific setup we’re facing some problems with this. Remember we configured ssh hosts and identity files for our custom github user in our ssh configuration. While the extensions shares the git configuration with the container, this does not apply to our ssh configuration. Changing our devcontainer.json to copy the files would not help as the container would then be bound to a specific system setup which would make a nonsense of working in a container.

My solution for this is the usage of another feature of devcontainers: dotfiles repositories. By setting up a repository with my ssh configuration and public key files I can ensure they will be present within the container after startup. The repository can be private but of has to be accessible by the default github user.

Withing the repository there is a directory .ssh containing the ssh configuration and public keys and an install script install.sh.

~/repos/dotfiles$ tree -a
.
├── .ssh
│   ├── config
│   └── pubkeys
│       ├── azdo.pub
│       ├── github.pub
│       └── github_custom.pub
└── install.sh

The ssh config is nearly identical with the one from my host system I showed previously, except for the removed configuration of the ForwardAgent and IdentityAgent. Only the configuration of the hosts with identity files are required.

Host github.com
  HostName github.com
  IdentityFile ~/.ssh/pubkeys/github.pub
  IdentitiesOnly yes

Host githubcustom
  HostName github.com
  IdentityFile ~/.ssh/pubkeys/github_custom.pub
  IdentitiesOnly yes

Host ssh.dev.azure.com
  Hostname ssh.dev.azure.com
  IdentityFile ~/.ssh/pubkeys/azdo.pub
  IdentitiesOnly yes

The install.sh script is limited to copying the .ssh directory to the home directory of the container.

#!/bin/bash

cp -r $(dirname $0)/.ssh ~/

Open vscode on the host (not running a container, otherwise the configuration options will no be available) to configure the devcontainer extension settings for usage of the repository as dotfile repository.

Set the repository, target path and install command so the dotfile repository will be checked out at the traget path and the install command be executed from inside the repository.

Now start a project with a devcontainer and check the setup. Note: If the container was built before, rebuild it to check out the dotfile repo and execute the script on startup.

If everything is setup the right way you should now be able to work with git and sign git commits using keys from the ssh agent inside a container

Marius Boden
Marius Boden is passionate about technology and software development. Being a hybrid of developer, architect and DevOps consultant, he can keep track of the big picture and help teams at any point in the development lifecycle to get to the next level.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts