Silmor . de
Site Links:
Impressum / Publisher

SSH and the Terminal of Doom

Making SSH automatable.

Most of the time SSH is used interactively and behaves correctly for this mode of operation: it asks for the password from the terminal you are working on or if there is no terminal it tries to find out how to ask for the password using a graphical tool.

The first mode is quite simple from SSH's perspective: it simply opens /dev/tty. If SSH is running on a terminal this will create an additional handle to access that exact terminal - regardless of whether input or output of the SSH process has been redirected to a file. If it fails SSH can safely assume that there is no terminal. The actual Unix concept behind this is the "controlling terminal". A process that is started from a terminal (i.e. it has this terminal as input and output file handles) is assigned this terminal as its controlling terminal. Any process spawned from it inherits that controlling terminal - even if input and output are closed or redirected. Opening /dev/tty gives each process an additional handle to its controlling terminal.

The second mechanism is a fall back so SSH still works if run in a script from a graphical environment. If opening /dev/tty fails SSH would normally give up, since it is unsafe to retrieve the passphrase from the input channel (you never know whether you need it and how often to ask - it all depends on circumstances). So SSH checks two variables: DISPLAY to find out whether it is running in a graphical environment (sorry Windows and Mac folks: SSH is a Unix tool, graphics means X11) and SSH_ASKPASS to find out whether there is a helper program that it can use to ask for a passphrase. Then it hands some description of the request as command line parameter to that helper program and expects the passphrase on its stdout (without an additional newline).

Things get tricky if you do not want to use SSH interactively, since there is no switch to enforce the use of SSH_ASKPASS. The only chance you have is to make sure SSH does not have a controlling terminal. Unfortunately there is no shell utility that helps with this (not even the nohup program), nor will any amount of opening sub-processes or redirection of input and output channels help in any way.

Fortunately there is a way to disconnect from a controlling terminal: the ioctl system-call can be used to explicitly make an open terminal non-controlling. But which file handle holds ther controlling terminal? To get around this question we can use the same technique that SSH is using: get a fresh handle via /dev/tty - if this succeeds we have our handle, if it failse there was no controlling terminal to begin with. Once we have this handle we can detach from it.

A very simple and re-useable detachment function looks like this:

/* ioctl definitions */
#include <sys/ioctl.h>
/* open */
#include <fcntl.h>
/* fprintf */
#include <stdio.h>

/* our detach-from-tty function */
void detach_tty()
{
  /* try to open the controlling TTY */
  int fd=open("/dev/tty",O_RDWR);
  if(fd<0){
    /* failed: we do not need to detach */
    fprintf(stderr,"No TTY to begin with.\n");
    return;
  }

  /* try to detach */
  if(ioctl(fd,TIOCNOTTY))
          fprintf(stderr,"Warning: unable to detach from TTY\n");
  else
          fprintf(stderr,"Detached.\n");

  /* close the handle, it is not needed anymore */
  close(fd);
}

It is now very easy to integrate this function into any other program. For example the code snippet below completes the detachment function into a simple wrapper that makes sure its argument is called without a controlling terminal:

#include <errno.h>
#include <string.h>

int main(int argc,char**argv)
{
  /* do we have something to do? */
  if(argc<2){
    fprintf(stderr,"Usage: %s command [arguments...]\n",argv[0]);
    return 1;
  }
  /* detach controlling TTY */
  detach_tty();
  /* call original program */
  execvp(argv[1],argv+1);
  /* normally execvp does not return*/
  /* the call failed for some reason */
  fprintf(stderr,"Unable to execute: %s.\n", strerror(errno));
  return 1;
}

Put all of the above code into a C-File (notty.c) and compile it with gcc -o notty notty.c to get an executable wrapper that you can use to automate SSH. To demonstrate it open an xterm and enter this:

SSH_ASKPASS=ssh-askpass ssh johndoe@somehost uname -a

SSH will ask for the password on the terminal before it sends the uname command to the remote host, because it was started inside a terminal. However:

SSH_ASKPASS=ssh-askpass ./notty ssh johndoe@somehost uname -a

will pop up a graphical prompt for the the password. Instead of the ssh-askpass program you can use any program that outputs the passphrase on stdout.

This can be used to fully automate SSH in scripts:

#!/bin/bash

# fake value for $DISPLAY, ssh insists on it
export DISPLAY=:99
# set password-knowing script
export SSH_ASKPASS=$HOME/mypwd-revealer.sh

# call SSH
$HOME/notty ssh johndoe@somehost dosomething.sh

The revealer script above could be something as simple as:

#!/bin/bash
echo -n "SuperSecritPassword"

Of course you have to make sure that storing the password in some kind of program does not lead to it being revealed to people who are not supposed to know it - so the least you have to do is set the correct permissions on that script (chmod 0700 mypwd-revealer.sh). Usually you have a good reason for automating SSH calls - for example making backups or remote monitoring - you should limit the things an attacker can do in case this passphrase is discovered during an attack. You can set a restricted shell, use non-privileged target accounts, restricted target environments, etc.

Often the goal is instead to call SSH from a graphical application - most of the time this will work out of the box, since most of the time GUI applications are started without a terminal. However from time to time you start from inside a terminal and wonder what went wrong - the SSH process inherited the controlling terminal from its parent GUI application. Adding the above function to your call of SSH will help:

void call_ssh(...)
{
  /* create a new process */
  int pid=fork();
  if(pid == 0){
    /* I'm the child, go detach */
    detach_tty();
    /* set the environment, we assume DISPLAY is already set */
    setenv("SSH_ASKPASS","/usr/bin/ssh-askpass");
    /* execute SSH */
    execvp("ssh",...);
    /*something went wrong*/
    exit(1);
  }else if(pid < 0){
    /* unable to fork... */
    raise_error(...);
  }
}

Portability Note: the code above has been written and tested on Linux. It should work out of the box on any Unix-like system, since it uses POSIX-standard functions. It might work on MacOS/X, but I did not test that. It does not work with MSys (using the MSys port of SSH), if anyone finds out how to detach from the (emulated) CTTY on MSys please let me know!


Webmaster: webmaster AT silmor DOT de