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.
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.
# 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:
# 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.
# 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
[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 firstname.lastname@example.org: > klist Ticket cache: FILE:/tmp/krb5cc_1000 Default principal: email@example.com Valid starting Expires Service principal 05/21/21 13:20:58 05/21/21 23:20:58 firstname.lastname@example.org 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.
# Always renew Kerberos creds at login if [ -f "/tmp/krb5cc_$(id -u)" ]; then kinit -R fi