WSL the Easy Way

6 minute read

I personally use Windows as my daily driver workstation operating system for managing both Windows and Linux servers. Most of the reason this is possible is due to Windows Subsystem for Linux (WSL) and the fact that I can have a full suite of Linux tools available without leaving my Windows workstation. In this article I humbly present my standard setup that makes WSL really usable.

Setting up the SSH Agent

Before we even install WSL I recommend setting up the built-in Windows OpenSSH’s SSH agent service. It stores your keys securely in the registry using DPAPI (encrypted with your user password) so you don’t need to worry about unlocking your agent constantly. As a bonus, it can be shared into WSL so you don’t need to run two agents or put your private keys inside the WSL filesystem.

The first step is to enable the service and start it

Get-Service -Name ssh-agent | Set-Service -StartupType Automatic -PassThru | Start-Service

Presuming you have a keypair already you can import it with ssh-add just like you would on Linux, or you can generate your own keys.

ssh-keygen.exe
ssh-add.exe

Set up WSL

I won’t walk you through how to install and setup WSL as there are numerous guides out there. The only recommendation I’ll make here is that you make your WSL username the same as your workstation username. This makes a few things easier down the road.

Set up wsl-relay

wsl-relay is a great tool maintained by Lex Robinson that really makes WSL a lot more useful. I use it to make my Windows OpenSSH agent available in WSL and also to make my GPG agent from gpg4win available in WSL as well.

We can build it ourselves in Go fairly easily. Here’s the script I use on Ubuntu:

#! /bin/console

# Install socat
sudo apt update && sudo apt install socat

# Install Golang
sudo add-apt-repository "ppa:longsleep/golang-backports" -y
sudo apt update && sudo apt install golang-go

# Grab the wsl-relay repo
REPO_PATH="github.com/lexicality/wsl-relay"
go get -d "$REPO_PATH"

# Create the bin directory if we don't have one
if [ ! -d "$HOME/bin" ];then
    mkdir "$HOME/bin"
fi

# Build a Windows binary for wsl-relay
GOOS=windows GO111MODULE="off" go build -o "$HOME/bin/wsl-relay.exe" "$REPO_PATH"

Now that we have wsl-relay.exe in our local bin directory we can fire it up any time we open a WSL session. I use the following snippet in my ~/.bashrc file to launch the appropriate wsl-relay pipes.

I’m using the presence of “explorer.exe” in my PATH to determine whether or not we are on a WSL session or a normal Linux session because I share the same .bashrc file among all of my home directories. This works really elegantly and has no obvious downsides that I can think of.

~/.bashrc

# Overrides when we're running in WSL
if [ $(type -p explorer.exe) ]; then
    # Set up gpg-agent relay if we have it available
    if [ -d ~/.gnupg ] && [ $(type -p socat) ] && [ $(type -p wsl-relay.exe) ]; then
        # Only set it up if it's not already running
        if ! ps aux | grep [s]ocat.*S.gpg-agent > /dev/null; then
            [ -e $HOME/.gnupg/S.gpg-agent ] && rm $HOME/.gnupg/S.gpg-agent
            ( setsid socat UNIX-LISTEN:$HOME/.gnupg/S.gpg-agent,fork, EXEC:'wsl-relay.exe --input-closes --pipe-closes --gpg',nofork & ) >/dev/null 2>&1
        fi
    fi

    # Set up ssh-agent if we have it available, and there isn't a real working SSH_AUTH_SOCK
    if [ -d ~/.ssh ] && [ $(type -p socat) ] && [ $(type -p wsl-relay.exe) ] && [ ! -S "$SSH_AUTH_SOCK" ]; then
        # Always set the environment variable
        export SSH_AUTH_SOCK="$HOME/.ssh/w32-ssh-agent"
        # Only set it up if it's not already running
        if ! ps aux | grep [s]ocat.*openssh-ssh-agent > /dev/null; then
            [ -e $HOME/.ssh/w32-ssh-agent ] && rm $HOME/.ssh/w32-ssh-agent
            ( setsid socat UNIX-LISTEN:$HOME/.ssh/w32-ssh-agent,fork, EXEC:'wsl-relay.exe --input-closes --pipe-closes --pipe //./pipe/openssh-ssh-agent',nofork & ) >/dev/null 2>&1
        fi
    fi
fi

It’s important to note here that I’ve chosen to use a “first in first out” approach to running my wsl-relays. This means that the first WSL session I open is responsible for all future sessions until it is closed. If the first session is closed you need to open a new one to start up new wsl-relay instances.

This works well for my daily workflow which usually involves opening a WSL session in Windows Terminal and leaving it open all day while I open and close many VSCode windows that run in WSL. This means my agent connection won’t die every time I close the most recent VSCode window because it took over the w32-ssh-agent socket in my ~/.ssh directory.

An alternative method would be to assign a randomized ssh-agent socket to each subsequent session and throw it in the temporary folder:

~/.bashrc

# Always set up a new random socket
export SSH_AUTH_SOCK="/var/tmp/w32-ssh-agent_$(( + $RANDOM % 1024))"
rm $SSH_AUTH_SOCK
( setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork, EXEC:'wsl-relay.exe --input-closes --pipe-closes --pipe //./pipe/openssh-ssh-agent',nofork & ) >/dev/null 2>&1

Fix Ansible quirks

I use Ansible fairly extensively in my work and do so in WSL often. By default Ansible will not use ansible.cfg if it is in a world-writable directory.

> ansible localhost -m debug
[WARNING]: Ansible is being run in a world writable directory, ignoring it as an ansible.cfg source. For more information see
https://docs.ansible.com/ansible/devel/reference_appendices/config.html#cfg-in-world-writable-dir

To work around this issue I detect when I am running in WSL in my ~/.bashrc and explicitly set my ANSIBLE_CONFIG environment variable to the default. If you set this environment variable then Ansible will happily use the config file in the local directory even though it is world-writeable.

~/.bashrc

# Overrides when we're running in WSL
if [ $(type -p explorer.exe) ]; then
    # Work around WSL permisions showing world write-able
    export ANSIBLE_CONFIG="./ansible.cfg"
fi

Kerberos for AD in WSL

If you’re on a Active Directory domain (which I’m guessing you are if you’re using WSL) you might want to be able to use those credentials inside of WSL. Setting up Kerberos inside of the WSL system is pretty straightforward and works well.

Once setup, you can use your Active Directory domain credentials in WSL to authenticate to servers that are also joined to the domain. I use this for running Ansible playbooks against Windows hosts with “ansible_winrm_transport: kerberos”.

The first step is obviously installing Kerberos

sudo apt install krb5-user

From there we’ll want to set up a very basic krb5.conf file that uses DNS to lookup all the Kerberos details

/etc/krb5.conf

[libdefaults]

default_realm = ad.example.net
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
rdns = true
forwardable = yes

At this point you should be able to run “kinit” and receive a Kerberos token from one of your domain controllers.

> kinit
Password for scott@ad.example.net:
> klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: scott@ad.example.net

Valid starting     Expires            Service principal
05/21/21 13:20:58  05/21/21 23:20:58  krbtgt/ad.example.net@ad.example.net
        renew until 05/28/21 13:20:53

You can now use this Kerberos credential to authenticate to network devices via SSH, WinRM, or any other service that can accept Kerberos authentication.

> ssh -o PreferredAuthentications=gssapi-with-mic jump.ad.example.net -V
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic
debug1: Next authentication method: gssapi-with-mic
debug1: Delegating credentials
debug1: Delegating credentials
debug1: Authentication succeeded (gssapi-with-mic).

You may also want to throw a gratuitous kerberos ticket renewal into your .bashrc as well to keep this ticket up to date. Conveniently this will show a warning if the credential is expired.

~/.bashrc

# Always renew Kerberos creds at login
if [ -f "/tmp/krb5cc_$(id -u)" ]; then
    kinit -R
fi