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.