SSH: Interactive ProxyCommand

I was involved in the creation of the sshephalopod project, which was an attempt to build an enterprise level authentication framework for SSH authentication using the SSH CA feature.

The project is based on a wrapper script that signs a user via a SAML identity provider and gets user’s public key signed for the further usage.

In one of the discussions I pointed out that such a wrapper script is not good for the end user experience and I proposed to provide the users with an excerpt for their ssh config file, so the functionality of sshephalopod would be transparent to the general usage scenario of the ssh tool.

The response was that ProxyCommand do not support interactivity. Well, as they say: The challenge is accepted :)

The following is my train of thoughts before I came up with a general solution on how to allow an interactive command to be used as the ProxyCommand in the ssh config file.

Before we start solving the problem at hand we need to create a test environment, so we would be able to confirm when we reached success. The task itself was very simple: we needed a host we could ssh into (an sshd daemon running on the local host would be sufficient), then we needed an interactive script, and a configuration block for the connection.

The configuration block is pretty simple (%h expands to localhost and %p expands to the port specified on the command line or to “22” otherwise):

[user@localhost ~]$ fgrep -A1 'Host localhost' ~/.ssh/config
Host localhost
ProxyCommand ~/bin/ %h %p

Since most of our research is going to be inside the interactive script you will see several incarnations of script’s body. The very first one was the following:

exec nc "$1" "$2"

At this point we just need to confirm that our test environment works as expected – the ssh session should be proxied through the nc command and we should be able to login under our own account via ssh to the localhost (my private key was added to the key manager with ssh-add, hence no password prompt was displayed):

[user@localhost ~]$ ssh galaxy@localhost
Last login: Thu Jul 21 01:30:21 2016

OK, we confirmed that we can establish a proxied connection and tunnel our ssh session through it.

Each interactive script or program relies on the communication channel with the user otherwise it could not be interactive. This channel comprises at least of two file descriptors: one for standard input and the other for standard output, so let’s check what descriptors are available for our script:

# on Linux the following line would be much simpler: ls -l /proc/$$/fd/, but
# on OS X they do not expose the open file descriptors through /proc, so I
# used "lsof" instead.
lsof -p $$ >&2
exec nc "$1" "$2"

If we try to connect now we should see something like the following (I am writing this article on an OS X machine so I provide the output from OS X, however this also works for Linux):

[user@localhost ~]$ ssh galaxy@localhost
bash 45225 user cwd DIR 1,2 612 893854 /Users/user/.ssh
bash 45225 user txt REG 1,2 628640 2329236 /bin/bash
bash 45225 user txt REG 1,2 625712 13892061 /usr/lib/dyld
bash 45225 user txt REG 1,2 385393734 13894121 /private/var/run/dyld_shared_cache_x86_64
bash 45225 user 0 PIPE 0x44d71099589485df 16384 ->0x44d7109951223d0f
bash 45225 user 1 PIPE 0x44d710995122454f 16384 ->0x44d71099512246af
bash 45225 user 2u CHR 16,1 0t631830 9705 /dev/ttys001
bash 45225 user 255r REG 1,2 219 15455259 /Users/user/bin/
Last login: Mon Jul 25 17:52:40 2016 from localhost

We are interested in file descriptors 0 (standard input), 1 (standard output), and 2 (standard error). As you can see the standard input and output are part of the pipes (presumably linking them to the parent ssh process) and standard error is pointing to our terminal session.

I could have occupied a bit more of the page space showcasing that if you try to communicate on standard input and/or output the ssh client will terminate since you will be messing with the SSH protocol flow, but I believe you will trust me on this :).

What can we do to interact with the user, yet to preserve the channel with the parent ssh process? Well, the answer is quite obvious:

The following version of the script demonstrates the implementation of the above logic:

exec 10<&0 11>&1 0<&2 1>&2
# start of the interactive behaviour
lsof -p $$
read -p "Type something: " I
echo "You typed: $I"
# finish of the interactive behaviour
exec 0<&10 1>&11
exec nc "$1" "$2"

I think it is time to test it :) :

[user@localhost ~]$ ssh galaxy@localhost
bash 45238 user cwd DIR 1,2 612 893854 /Users/user/.ssh
bash 45238 user txt REG 1,2 628640 2329236 /bin/bash
bash 45238 user txt REG 1,2 625712 13892061 /usr/lib/dyld
bash 45238 user txt REG 1,2 385393734 13894121 /private/var/run/dyld_shared_cache_x86_64
bash 45238 user 0u CHR 16,1 0t640407 9705 /dev/ttys001
bash 45238 user 1u CHR 16,1 0t640407 9705 /dev/ttys001
bash 45238 user 2u CHR 16,1 0t640407 9705 /dev/ttys001
bash 45238 user 10 PIPE 0x44d710995122378f 16384 ->0x44d71099589494ff
bash 45238 user 11 PIPE 0x44d7109951223d0f 16384 ->0x44d710995122454f
bash 45238 user 255r REG 1,2 135 15455285 /Users/user/bin/
Type something: This is a test
You typed: This is a test
Last login: Mon Jul 25 17:57:05 2016 from localhost
$ logout
Connection to localhost closed.

Mission accomplished! :)

I hope this small article would help somebody to design better wrappers around SSH. Keep in mind that you could always optimise it further. For example, recent versions of OpenSSH support passing of a file descriptor from the ProxyCommand script, so if you have a decent netcat tool that supports the “-F” option (fdpass) you could get native performance for the ssh communication link with no proxy process hanging around.

P.S. if you have any questions do not hesitate to comment.