As I mentioned in my previous post, I switched to using SSH key auth for GitHub and Azure DevOps Repos a long time ago and found it a positive experience. At first I was a bit lazy and didn’t use passphrases on my keys, and just kept a copy of my keys in the
.ssh folder in my User folder in Windows and another copy in
~/.ssh in WSL.
For day-to-day working this worked okay, but I finally got round to adding passphrases to my keys a while back and was less happy with the setup at that point. My previously suppressed niggles around having the keys in multiple places re-surfaced once I had to add handle passphrases in multiple systems on the same machine!
As it happens, recent versions of Windows ship with the OpenSSH Agent and Server but I didn’t want to have two SSH Authentication services each with their own set of keys. My last post shows how to get git in Windows to use the OpenSSH Agent to retrieve keys. In this post, I’ll walk through the journey to get SSH in WSL using keys from the Windows OpenSSH Agent. For details on installing and setting up the Windows OpenSSH Agent see the docs.
If you just want to set up the WSL SSH forwarding then skip to the final solution! Otherwise, let me take you on a tour…
As I mentioned, my most common usage of SSH keys day-to-day is as a way to authenticate to git remotes. This lead me to this page in the GitHub docs which discusses SSH Agent forwarding and mentions the
SSH_AUTH_SOCK environment variable.
After some reading it turns out that
SSH_AUTH_SOCK controls the path to the UNIX socket that is used by SSH tools to communicate with the SSH Agent. This seemed like an interesting start, and fortunately I’d previously stumbled across the npiperelay from John Starks. The docs and examples for
npiperelay have some examples for using
npiperelay with the
socat utility as a way of forwarding Linux sockets in WSL to named pipes in Windows - sounds perfect!
First off, I grabbed
# replace with the appropriate install for non-Debian/Ubuntu distros ;-) sudo apt install socat
Next up, I built
npiperelay, but happily John merged a PR that added releases, so you can now grab the latest release from GitHub. Once downloaded, extract the
npiperelay.exe and place it somewhere in your
The docker-relay example from
npiperelay shows how to forward the
/var/run/docker.sock socket to the
//./pipe/docker_engine named pipe.
# Example from https://github.com/jstarks/npiperelay/blob/master/scripts/docker-relay exec socat UNIX-LISTEN:/var/run/docker.sock,fork,group=docker,umask=007 EXEC:"npiperelay.exe -ep -s //./pipe/docker_engine",nofork
I took a bit of time to read up on
socat. My understanding of the previous command is that it listens to the
/var/run/docker.sock UNIX socket and when it gets a connection it launches the
EXEC command. So in this case it starts an instance of
npiperelay.exe using the specified arguments and forwards the data received on the UNIX socket to the input stream for
npiperelay. On the other side,
npiperelay takes the data sent to its input stream and forwards it to the
//./pipe/docker_engine named pipe.
In this way,
socat combine to be a way to forward from a UNIX socket to a Windows named pipe - pretty cool!
Armed with new tools and knowledge, I deleted my SSH keys from
~/.ssh in WSL and then ran the following commands to set
SSH_AUTH_SOCK and start socat.
export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &
Once that is set up I gave it a test to authenticate with GitHub (note that I’ve already added the key to GitHub and the Windows OpenSSH Agent):
$ ssh -T email@example.com Hi stuartleeks! You've successfully authenticated, but GitHub does not provide shell access.
Or almost :-)
It turns out that there are a few challenges with this, at least there are when you put it into your
.bash_profile so that it runs automatically when you start your terminal.
In this section we’ll walk through the series of tweaks that I ended up making to the script. This will help to understand the script but feel free to skip ahead to the final solution if you are keen to get this set up.
The first issue I hit was running multiple instances of the terminal. To overcome this I added a check via
ps -aux to look to see if the
npiperelay command was running.
My first attempt at this was:
ALREADY_RUNNING=$(ps -aux | grep -q "npiperelay.exe -ei -s //./pipe/openssh-ssh-agent"; echo $?)
Unfortunately, this always returned
0. Dropping the
-q and running it interactively, I realised that the
ps... | grep... command was being listed in the
ps output, so it alwasy got a match. Fortunately, a colleague had pointed me to a nice way round this a while back:
ALREADY_RUNNING=$(ps -aux | grep -q "[n]piperelay.exe -ei -s //./pipe/openssh-ssh-agent"; echo $?)
Note the square brackets around the
[n]piperelay.exe. That gives a regular expression that requires that character to be an
n, so we still match the original search string for the
grep stage. But, it changes the command so that it no longer matches itself!
With this in place, I still hit an issue where sometimes it wasn’t finding the existing process. This took me a little longer to track down, but the issue was that the
ps output gets truncated to try to fit in the terminal width. Adding the
-ww to the
ps command forced it to not truncate:
ALREADY_RUNNING=$(ps -auxww | grep -q "[n]piperelay.exe -ei -s //./pipe/openssh-ssh-agent"; echo $?)
Unfortunately, my notes for this part are missing some details, but I hit a scenario where sometimes the socket already existed.
For this I added a test using the
-S condition (a list of condition expressions can be found here).
socat command isn’t running then I added this check before starting it:
if [[ -S $SSH_AUTH_SOCK ]]; then # not expecting the socket to exist as the forwarding command isn't running (http://www.tldp.org/LDP/abs/html/fto.html) echo "removing previous socket..." rm $SSH_AUTH_SOCK fi
setsid and suppressing the output resulted in
(setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &) > /dev/null 2>&1
At this point, it can all be put together…
The end result is that once
socat is installed in WSL, and
npiperelay is (See how to get the tools), you can add this script to your
Note that this also requires that you have set up the SSH Agent in Windows and added your keys to it (as mentioned in the previous post).
# Configure ssh forwarding export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock # need `ps -ww` to get non-truncated command for matching # use square brackets to generate a regex match for the process we want but that doesn't match the grep command running it! ALREADY_RUNNING=$(ps -auxww | grep -q "[n]piperelay.exe -ei -s //./pipe/openssh-ssh-agent"; echo $?) if [[ $ALREADY_RUNNING != "0" ]]; then if [[ -S $SSH_AUTH_SOCK ]]; then # not expecting the socket to exist as the forwarding command isn't running (http://www.tldp.org/LDP/abs/html/fto.html) echo "removing previous socket..." rm $SSH_AUTH_SOCK fi echo "Starting SSH-Agent relay..." # setsid to force new session to keep running # set socat to listen on $SSH_AUTH_SOCK and forward to npiperelay which then forwards to openssh-ssh-agent on windows (setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &) >/dev/null 2>&1 fi
With all this in place your SSH keys will be handled by the Open SSH Agent in Windows and SSH in WSL will access them from there!
P.S. If you liked this, you may also like my book “WSL 2: Tips, Tricks and Techniques” which covers tips for working with WSL 2, Windows Terminal, VS Code dev containers and more https://wsl.tips/book :-)