Home Tools Blog

A Known SSH Socket for Tmux Using a known, shared SSH socket to enable agent forwarding through an existing tmux session

Artisanal SSH socket remapping

I spend a lot of my development time working on a remote server, and regularly need to connect to a number of additional servers, all over SSH. Plus I often push and pull git-controlled code via SSH to yet more remote servers.

To keep track of everything, I do 100% of my work within tmux. To let me chain my SSH connections, nearly every connection uses ForwardAgent. Unfortunately, this doesn't work for long. When I reconnect to a server and reattach my tmux session, I am suddenly unable to chain my connections!

The problem here is that my SSH Agent has created a new socket for my new connection. This works fine by itself, but when I reattach the already existing tmux session, I no longer have any reference to the new socket. Inside of tmux, SSH will try to use the socket in use at the time the session was created, which probably no longer exists.

So what to do? The obvious solution is to simply close my tmux session when I disconnect and create a new one with every new connection. But this has problems.

  • First, what if I accidentally disconnect? Maybe I've lost my network connection, or somehow accidentally hit ~.. I want to get back into my session as quickly and easily as possible.
  • Second, what if I want to save my panes when I disconnect? Maybe there's some long-running process I want to keep. Or maybe I simply don't want to have to recreate my session every time I connect (though some of this can be solved by a project like tmuxinator).

Obviously, a better solution would be to just fix the problem and get tmux to always use the current socket. Additionally, I want to be sure to support using tmux within SSH within tmux, chained arbitrarily. The answer is to always put the socket in a known location and hook everything up to use it.

Rather than try to devise some solution to signal to tmux what the current socket file is, it will be much easier to use a symbolic link. Whenever we create a new socket, we'll simply override the existing link with a link to the new socket.

We need a name for this symbolic socket, so how about /tmp/ssh-agent-$USER-screen. We're putting it in /tmp/ since it doesn't matter too much if this is overwritten or cleaned up. We're also using the USER environment variable to keep sockets separate for different users. At the end, I'm putting -screen since this is sort-of more general than -tmux, but it can really be whatever, or even removed.

Now, creating a symbolic link is all fine and good, but what do we actually link to? Unfortunately there's no great built-in way to grab the current socket all the time. But there's no need to re-invent the wheel, we can use the proven ssh-find-agent tool. So let's put that in a useful location:

git clone git@github.com:wwalker/ssh-find-agent.git ~/lib/ssh-find-agent

We'll use the "automatic" -a option, which will find the active SSH agent and store it in SSH_AUTH_SOCK for us. But, if there is no active SSH session, nothing useful will happen, so we'll want to get the SSH agent started.

# Source the script first
. ~/lib/ssh-find-agent/ssh-find-agent.sh
ssh_find_agent -a

# If nothing happened, we need to start up the ssh-agent
if [ -z "$SSH_AUTH_SOCK" ]
then
  eval $(ssh-agent) > /dev/null
  ssh-add -l >/dev/null || alias ssh='ssh-add -l >/dev/null || ssh-add && unalias ssh; ssh'
fi

Now that we have the socket, we just need to make (or override) that symbolic link so it can be found later.

SOCK="/tmp/ssh-agent-$USER-screen"
if test $SSH_AUTH_SOCK && [ $SSH_AUTH_SOCK != $SOCK ]
then
  rm -f /tmp/ssh-agent-$USER-screen
  ln -sf $SSH_AUTH_SOCK $SOCK
  export SSH_AUTH_SOCK=$SOCK
fi

Putting it all together, we'll find the active socket or create it, then make a known symblic link. Now we just have to do this everywhere the socket is needed. This is the most annoying part, though it can be relieved with a tool like sshrc. The full process will need to be added to the ~/.bashrc or ~/.zshrc on your host system, as well as every system you want to chain tmux and SSH sessions from.

To be clear about where this needs to happen, if your chain looks like this:

host → tmux → (remote 1) → tmux → (remote 2) → tmux → (remote 3)
            ↘
              (remote 4) → tmux → (remote 5)

Then you would need to have this set-up on host, (remote 1), (remote 2) and (remote 4), but not the last two remotes. If you think of these chain connections as a tree, the socket mapping is not needed on the leaves. Technically it's also not needed on any nodes on which you're not using tmux, provided you use ForwardAgent.

So there we have it, the SSH socket symbolically linked to a known location. After cloning ssh-find-agent, here's the complete script to add to your shell login script as required:

# Known SSH Socket for tmux
# https://blog.jmthornton.net/p/tmux-known-socket

. ~/lib/ssh-find-agent/ssh-find-agent.sh
ssh_find_agent -a
if [ -z "$SSH_AUTH_SOCK" ]
then
  eval $(ssh-agent) > /dev/null
  ssh-add -l >/dev/null || alias ssh='ssh-add -l >/dev/null || ssh-add && unalias ssh; ssh'
fi

# Predictable SSH authentication socket location so tmux can find it
SOCK="/tmp/ssh-agent-$USER-screen"
if test $SSH_AUTH_SOCK && [ $SSH_AUTH_SOCK != $SOCK ]
then
  rm -f /tmp/ssh-agent-$USER-screen
  ln -sf $SSH_AUTH_SOCK $SOCK
  export SSH_AUTH_SOCK=$SOCK
fi