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
git@github.com: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='custommail@customdomain.com' --config user.signingkey='ssh-ssh-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxhFVyilOVwuMW8AAmtHqemrJDzT7d14VAR4Rcj81u4' $(echo $1 | sed 's/git@github.com/git@githubcustom/g'); }; f'
# clone repo
git clone-custom git@github.com: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='custommail@customdomain.com'
--config user.signingkey='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxhFVyilOVwuMW8AAmtHqemrJDzT7d14VAR4Rcj81u4'
$(echo $1 | sed 's/git@github.com/git@githubcustom/g');
}
# call function
f git@github.com: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 git@github.com: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
remote.origin.url=git@github.com: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@github.com:shrutikapoor08/devjoke.git (fetch)
origin git@github.com: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 git@github.com: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
user.email=custommail@customdomain.com
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