SSH: Interactive ProxyCommand

I was involved in the creation of the sshephalopod project, which is 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. OK, 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):

$ fgrep -A1 'Host localhost' ~/.ssh/config
Host localhost
 ProxyCommand ~/bin/interactive.script.sh %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:

$ cat ~/bin/interactive.script.sh
#!/bin/bash
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):

$ ssh galaxy@localhost
Last login: Thu Jul 21 01:30:21 2016
home:~ galaxy$

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:

$ cat ~/bin/interactive.script.sh
#!/bin/bash
# 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):

$ ssh galaxy@localhost
COMMAND   PID   USER   FD   TYPE             DEVICE  SIZE/OFF     NODE NAME
bash    45225 galaxy  cwd    DIR                1,2       612   893854 /Users/galaxy/.ssh
bash    45225 galaxy  txt    REG                1,2    628640  2329236 /bin/bash
bash    45225 galaxy  txt    REG                1,2    625712 13892061 /usr/lib/dyld
bash    45225 galaxy  txt    REG                1,2 385393734 13894121 /private/var/run/dyld_shared_cache_x86_64
bash    45225 galaxy    0   PIPE 0x44d71099589485df     16384          ->0x44d7109951223d0f
bash    45225 galaxy    1   PIPE 0x44d710995122454f     16384          ->0x44d71099512246af
bash    45225 galaxy    2u   CHR               16,1  0t631830     9705 /dev/ttys001
bash    45225 galaxy  255r   REG                1,2       219 15455259 /Users/galaxy/bin/interactive.script.sh
Last login: Mon Jul 25 17:52:40 2016 from localhost
home:~ galaxy$
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: we have a pointer to the terminal session (file descriptor 2, the standard error, points to the terminal), so we just need to save pointers to the pipes' ends, re-open standard input and output with the terminal before we interact with the user, and restore these file descriptors back once we are ready to hand over the ssh session:

$ cat ~/bin/interactive.script.sh
#!/bin/bash
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 :) :

$ ssh galaxy@localhost
COMMAND   PID   USER   FD   TYPE             DEVICE  SIZE/OFF     NODE NAME
bash    45238 galaxy  cwd    DIR                1,2       612   893854 /Users/galaxy/.ssh
bash    45238 galaxy  txt    REG                1,2    628640  2329236 /bin/bash
bash    45238 galaxy  txt    REG                1,2    625712 13892061 /usr/lib/dyld
bash    45238 galaxy  txt    REG                1,2 385393734 13894121 /private/var/run/dyld_shared_cache_x86_64
bash    45238 galaxy    0u   CHR               16,1  0t640407     9705 /dev/ttys001
bash    45238 galaxy    1u   CHR               16,1  0t640407     9705 /dev/ttys001
bash    45238 galaxy    2u   CHR               16,1  0t640407     9705 /dev/ttys001
bash    45238 galaxy   10   PIPE 0x44d710995122378f     16384          ->0x44d71099589494ff
bash    45238 galaxy   11   PIPE 0x44d7109951223d0f     16384          ->0x44d710995122454f
bash    45238 galaxy  255r   REG                1,2       135 15455285 /Users/galaxy/bin/interactive.script.sh
Type something: This is a test
You typed: This is a test
Last login: Mon Jul 25 17:57:05 2016 from localhost
home:~ galaxy$ 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 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.

Comments

Popular posts from this blog

Should we use ‘sudo’ for day-to-day activities?

Transparent SSH host-jumping (Advanced)