After installing Bash on Ubuntu on Windows I realized some interesting side effects related to how processes and daemons in the Unix environment are handled. Running a process in the background, or daemonizing a process, will work so long as there is a Bash session open on Windows. Once the last window is closed, all of the processes are cleaned up and killed.

I use ssh keys for authentication when connecting to remote servers which requires the use of ssh-agent. I can run this program manually and it will work so long as there is at least one bash session running on my computer, but once I close the last window the ssh-agent is killed and my keys are unloaded. I've found a couple guides online regarding ssh-agent and WSL specifically, but most of them assume the keys you are using are not password protected.

To remedy this situation, I managed to find a way to create a hidden terminal session that runs ssh-agent in foreground mode when I login to my computer which persists through sleeps and hibernations. This way, ssh-agent will run and stay running from the moment I login until the moment I logout (which is almost never, unless I reboot).

Create the Scripts

The short version of this is a VBS script is executed by the Windows Task Scheduler at login, which runs a .bat script in a hidden terminal window, which calls bash, which calls a bash script, which calls ssh-agent if it is not already running.

 Task Scheduler
 -> start-hidden.vbs
 -> start-ssh-agent.bat
 -> bash
 -> start-ssh-agent
 -> ssh-agent

It sounds confusing, and it unfortunately is, but it works!

To get started, I created a symlink in my Unix home directory that maps to my home directory on Windows to simplify things.

ln -s /mnt/c/Users/dave/ ~/userprofile

Now, I can access ~/userprofile which is a link to my users folder on Windows. I created a directory for scripts in My Documents and ~/bin using the following commands.

mkdir ~/bin
mkdir ~/userprofile/Documents/scripts

Note: all of the scripts below can also be found on GitHub

https://github.com/bahamas10/windows-bash-ssh-agent

start-hidden.vbs

This script will call the bat script in a hidden command prompt window

~/userprofile/Documents/scripts/start-hidden.vbs

Dim WinScriptHost
Set WinScriptHost = CreateObject("WScript.Shell")
WinScriptHost.Run Chr(34) & "%userprofile%\Documents\scripts\start-ssh-agent.bat" & Chr(34), 0
Set WinScriptHost = Nothing

start-ssh-agent.bat

This script will call a bash script that's responsible for starting ssh-agent if it is not already running. Note that -c is required with bash so ~ gets resolved.

~/userprofile/Documents/scripts/start-ssh-agent.bat

\Windows\System32\bash.exe -c "~/bin/start-ssh-agent"

start-ssh-agent

Finally, this is the meat of the logic here. This bash script will start ssh-agent if it is not already running.

~/bin/start-ssh-agent

#!/usr/bin/env bash
#
# Start ssh-agent if it is not running (by becoming it)
# Intended for use on Bash for Windows using the WSL
#
# Author: Dave Eddy <dave@daveeddy.com>
# Date: October 09, 2017
# License: MIT

# Could be any file - nothing intrinsically valuable about ~/.ssh/environment
envfile=~/.ssh/environment

# Ensure the environment file exists and has its permissions properly set.
# Source the file - if it was created by this script the source will
# effectively be a noop.
mkdir -p "${envfile%/*}"
touch "$envfile"
chmod 600 "$envfile"
. "$envfile"

# Check if the daemon is already running
if [[ -n $SSH_AGENT_PID ]] && kill -0 "$SSH_AGENT_PID" 2>/dev/null; then
    # The PID is up but it could have been recycled - attempt to list keys.
    # This will exit with 2 if the SSH_AUTH_SOCK is broken.
    ssh-add -l &>/dev/null
    if (($? != 2)); then
        echo "alreading running: $SSH_AGENT_PID"
        exit 1
    fi
fi

# Overwrite what is in the envfile to start a fresh ssh-agent instance
echo "# Started $(date)" > "$envfile"

# For some reason, this line doesn't get emitted by ssh-agent when it is run
# with -d or -D.  Since we are starting the program with exec we already know
# the pid ahead of time though so we can create this line manually
echo "SSH_AGENT_PID=$$; export SSH_AGENT_PID" >> "$envfile"

# Become ssh-agent and run forever
exec ssh-agent -D >> "$envfile"

Install the Service

The next step is to have Windows start the ssh-agent when you login. To do this, open the task scheduler by opening the start menu and searching for "Task Scheduler" and opening the main result.

  1. Click Create Basic Task... on the right
  2. Enter a name like ssh-agent bash
  3. Set Trigger to When I log on
  4. Set Action to Start a program
  5. Set Program/script to C:\Users\dave\Documents\scripts\start-hidden.vbs
  6. Finish up

The next thing to do is modify the tasks conditions and settings.

For the conditions, uncheck pretty much everything. Basically, we want this service to start regardless of battery/AC state.

task-conditions

For the settings, uncheck pretty much everything again except for allowing the task to run on demand.

task-settings

This service will now run when you login to the computer, but for now you can click the newly created task and select "Run" on the right to start it manually this once.

Update your bashrc

The final step is to have your Windows bashrc file source the ssh environment file that is written by the start-ssh-agent script.

echo '. ~/.ssh/environment > /dev/null' >> ~/.bashrc

Here is the section in my bashrc that actually detects if I'm on WSL to then source in the env file.

https://github.com/bahamas10/dotfiles/commit/fd7047243293674ed38f69ce5653104373ac727b

Done!

Now, whenever you open a bash shell ssh-agent will be running and its required environmental variables will be set for you to use. You can verify it is running using ssh-add -l.