Friday, July 4, 2014

Automate SSH Login and TMUX Session and Window Setup

Modern Time Technology

That was it... I was finally fed up with amount of time I was spending to SSH into each host manually. I was repeating the same procedure probably hundreds of times every day... Typing the ssh command and the host name, waiting for the initial prompt, entering my password, typing the sudo -i command to get root access, and entering my password again. Just as expected from a modern time worker. But wait, there was more to deal with...

Working with a lot of servers, I need to use my favorite terminal emulator to organize and manage the number of sessions. This means that every window has to be properly named and assigned to a specific session. So before logging into each server, I had to switch to the correct TMUX session (or create a new one), create a new TMUX window, name the window properly, and finally move on to the SSH stage.

I was aware of a script that was created by an ex-colleague. The script automated the process by storing the credentials in a key chain; however, it was written in Python and designed to run on a Mac. Not to mention, that long promised Kerberos implementation was just a mirage. I was more likely to see Unicorns running around in the office than a Kerberos solution. What I needed was a native Linux implementation written with BASH!

Before I begin, here are some details about my environment. I run the scripts on my Fedora 20 instance accessing primarily CentOS servers. The SSH hop script is dependent on components such as the KDE Wallet Manager and the expect tool. The TMUX hop script requires the TMUX tool as can be easily guessed from its name.

KDE Wallet Manager Configuration

The first step is to utilize the KDE Wallet Manager to store the SSH username and password in a secure manner. I simply created a new wallet named ssh and protected it with a GPG key. In addition, I created two entries named ssh_username and ssh_password under the Password folder.

SSH Login Automation

This awesome blog post about how to access the KWalletManager via the command line interface was the key for this implementation. The first time the ssh.hop script is called, KDE Wallet Manager responds with a prompt to decrypt the wallet and from then on you can use the script without any prompts (provided that you allow access not just once). Here is the script:

ssh.hop

#!/bin/bash

if [ "$#" -eq 0 ]
then
    echo "Usage: $0 <hostname> [<username>]"
    exit
fi

hostname=$1

namedPipe=/tmp/expect.$$
trap "rm -f $namedPipe; exit;" SIGHUP SIGINT SIGQUIT SIGTERM EXIT

walletClient="ssh.hop"
walletName="ssh"
walletUsernameEntry="ssh_username"
walletPasswordEntry="ssh_password"

walletId=$(qdbus org.kde.kwalletd /modules/kwalletd org.kde.KWallet.open "$walletName" 0 "$walletClient")

if [ -z "$walletId" ]
then
    echo "Unable to get wallet id."
    exit
fi

if [ -z "$2" ]
then

    username=$(qdbus org.kde.kwalletd /modules/kwalletd readPasswordList $walletId Passwords "$walletUsernameEntry" "$walletClient" | awk '{print $2}')
    
    if [ -z "$username" ]
    then
        echo "Unable to get username from wallet."
        exit
    fi

else
    username=$2
fi

password=$(qdbus org.kde.kwalletd /modules/kwalletd readPasswordList $walletId Passwords "$walletPasswordEntry" "$walletClient" | awk '{print $2}')

if [ -z "$password" ]
then
    echo "Unable to get password from wallet."
    exit
fi

export HISTIGNORE="expect*"

mkfifo $namedPipe

echo "
    spawn ssh -o StrictHostKeyChecking=no -t $hostname sudo -i
    expect {
        \"?assword:\" { send \"$password\r\" }
        timeout exit
    }
    expect {
        \"password for $username:\" { send \"$password\r\" }
        timeout exit
    }
    interact" > $namedPipe &
    
expect -f $namedPipe
rm -f $namedPipe

A couple of things to note here... The named pipe is required so that we can background the echo statement and exit. If we don't utilize a named pipe and use a regular echo "expect commands" | expect -f - call, expect fails to work properly since the first process is needed to be kept alive. Because, by default, interact expects the user to be writing stdin and reading stdout of the expect process itself. You can prevent the pipe from closing by running a command like (echo "expect commands"; cat) | expect -f - but that means your process list would be littered with echo statements containing clear text passwords. Long story short, to prevent that from happening, a named pipe was used to keep the process list clean.

TMUX Session and Window Setup Automation

As I mentioned before I also needed a way to manage my TMUX session. So I wrote the tmux.hop script to automate TMUX session and window setup and kick of an SSH login right away utilizing the ssh.hop script included above.

tmux.hop

#!/bin/bash

if [ "$#" -eq 0 ]
then
    echo "Usage: $0 <hostname> [<session>] [<username>]"
    exit
fi

h=$1
s=$2
u=$3

tmux=$TMUX

# if a session name provided
if [ ! -z "$s" ]
then

    tmux has-session -t "$s" &>/dev/null
    if [ "$?" -ne 0 ]
    then
        TMUX=""
        tmux new-session -d -s "$s"
    fi
    
    tmux new-window -t "$s" -n "$h" "ssh.hop $h $u"
    
    if [ -z "$tmux" ]
    then
        tmux attach-session -d -t "$s"
    else
        tmux switch-client -t "$s"
    fi

    tmux select-window -t "$s:$h"
    
# no session name provided
else
    
    if [ -z "$tmux" ]
    then
        echo "No current tmux session."
        exit
    fi
    
    tmuxCurrentSession=$(tmux display-message -p '#S')
    tmux new-window -n "$h" "ssh.hop $h $u"
    tmux select-window -t "$tmuxCurrentSession:$h"

fi

Conclusion

This solution seems to be kinda ugly and not practical; however, I am already saving a lot of cycles in my workday by using these scripts. So in my book, the technology works... :)