Skip to content

Configuring sshpass

Created on Feb 25, ’23 ・ Updated on Apr 2, ’23

Summary: how to encrypt and store a password into a file with GPG, and then decrypting it to provide it to sshpass. Also, some Zsh completion configuration for sshpass. Quick demo below with some dummy users and hosts:

Overview

sshpass is a command-line tool allowing a user to connect to SSH hosts without having to interactively input the user's password. The password can be provided to sshpass in different ways:

  • Via the command-line:

    sshpass -p pwd123 ssh user@host
    
    The least secure option: the password will appear for a short time in the output of ps aux | grep sshpass before sshpass "makes a mininal attempt to hide the password". It will also appear in the shell's history.

  • With an environment variable:

    export MYENV=pwd123
    sshpass -eMYENV ssh user@host
    
    The password may also appear when doing ps axe | grep MYENV before sshpass unsets the environment variable.

  • Using a file:

    echo pwd123 > pwd.txt
    chmod 600 pwd.txt
    sshpass -f pwd.txt ssh user@host
    
    This is a less unsecure method but still implies storing the password in plaintext.

  • Using an encrypted file:

    echo pwd123 | gpg --personal-cipher-preferences AES256 -c -o pwd.gpg
    sshpass -f <(gpg -dq pwd.gpg) ssh user@host
    
    This is probably the least unsecure method as the password is stored encrypted in a file, and the shell's process substitution, i.e. <(command), is used to create an anonymous pipe (which is what sshpass recommends albeit with -d instead of -f).

GPG

What is GPG and why use it with sshpass? GNU Privacy Guard is an implementation of the OpenPGP standard, it was created as a free-software replacement for PGP, which is developed by Broadcom Inc. under a proprietary software license.

GPG has a daemon called gpg-agent which, among other things, caches passphrases used to encrypt private keys, so that the user is not required to provide his passphrase everytime he needs it.

For example, when encrypting a password using a symmetric cipher (e.g. AES256) with the command below, gpg-agent will cache the passphrase for a certain amount of time before requesting it again from the user.

# this command will ask for a passphrase
$ echo mypwd123 | gpg --symmetric -o myfile
$ cat myfile
# the encrypted password

# this command will not ask for the passphrase, as it has been cached
$ gpg -d myfile
gpg: AES256.CFB encrypted data
gpg: encrypted with 1 passphrase
mypwd123

Thus, when combining sshpass and gpg, the user will only be required to input the passphrase on the first call to gpg -d. On certain OSes, it's also possible to store the passphrase in the OS's keyring app (e.g. GNOME Keyring). In this case, gpg will never ask for the passphrase, the keyring is unlocked when the user logs in with his password.

Script & aliases

Script to create the file with the encrypted password and some alias definitions for sshpass.

#!/usr/bin/env sh
set -eu

SSHPASS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/sshpass"
SSHPASS_FILE="$SSHPASS_DIR/password.gpg"
mkdir -p "$SSHPASS_DIR"

# don't echo password
printf "Input password to be used for sshpass: "
trap 'stty echo' INT EXIT
stty -echo
read PWD
printf "\n"

echo "$PWD" | gpg --personal-cipher-preferences AES256 -c -o $SSHPASS_FILE
echo Password encrypted in $SSHPASS_FILE
SSHPASS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/sshpass"
SSHPASS_FILE="$SSHPASS_DIR/password.gpg"

if [ -r "$SSHPASS_FILE" ]
then
    alias ssh='sshpass -f <(gpg -dq "$SSHPASS_FILE") ssh -o StrictHostKeyChecking=no'
    alias scp='sshpass -f <(gpg -dq "$SSHPASS_FILE") scp -o StrictHostKeyChecking=no'
    alias ssho='/bin/ssh'
    alias scpo='/bin/scp'
fi

Zsh completion

ssh

Some basic settings to get started with Zsh completion system as well as improvements for the ssh command.

# .zshrc

# 0. general completion settings ############################################# #

# initialize zsh completion system `compsys'
autoload -Uz compinit
compinit -i

# disabled by default, try to make completion list smaller
setopt list_packed

# enabled by default, always list choices when ambiguous completion
setopt auto_list

# enabled by default, when completing an unambiguous prefix on cli, do list
# possible choices
setopt list_ambiguous

# disabled by default, when ambiguous completion, don't require another TAB to
# select the first choice, but select the first choice immediately (then use
# arrows or TAB to navigate between items)
setopt menu_complete

# Enable the interactive mode of the menu selection which allows to filter
# listed choices by continuing to type letters on the cli. May be too much as it
# prevents TAB from being used to go to the next item (see [1]). Arrow keys
# can be used but then the interactive mode will exit (see [2]).
zstyle ':completion:*' menu select interactive

# load menu selection module to define key bindings for the menuselect keymap
zmodload -i zsh/complist

# [1] TAB or Ctrl-I to move to next item while in interactive mode (but will
# exit interactive mode). Shift-TAB to go to previous item in completion.
bindkey -M menuselect '^I' vi-forward-char
bindkey -M menuselect '^[[Z' vi-backward-char

# [2] Ctrl-L to bring back the interactive mode
bindkey -M menuselect '^L' vi-insert

# Ctrl-O to accept completion item and execute command immediately
bindkey -M menuselect '^O' .accept-line

# in-word case-insensitive completion. From:
#   https://stackoverflow.com/a/68794830
#   https://stackoverflow.com/a/69014927
zstyle ':completion:*' matcher-list \
    'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' \
    '+r:|[._-]=* r:|=*' \
    '+l:|=*'

# 1. ssh hosts completion #################################################### #

# Retrieve hosts from ~/.ssh/{config,known_hosts{,2}}. Note: if the ssh config
# option HashKnownHosts is enabled, the hosts in known_hosts are hashed and thus
# can't be used here. From: https://serverfault.com/a/170481

typeset -U h # remove duplicates
h=(${${${(@M)${(f)"$(cat ~/.ssh/config)"}:#Host *}#Host }:#*[*?]*})
h=($h ${${${(f)"$(cat ~/.ssh/known_hosts{,2} || true)"}%%\ *}%%,*}) 2>/dev/null
zstyle ':completion:*:(ssh|scp):*:hosts' hosts $h

# 2. ssh users completion #################################################### #

# show only wanted users (or disable users completion by leaving the list empty)
zstyle ':completion:*:(ssh|scp):*:users' users myuser produser

# 3. separate users and hosts in menu selection ############################## #

zstyle ':completion:*:*:(ssh|scp):*:descriptions' format '%F{green}-- %d%f'
zstyle ':completion:*:*:(ssh|scp):*:*' group-name ''

sshpass

Put the script below in a file named _sshpass which must be in a folder that is in the fpath. If you modify the fpath, remember to do it before calling compinit. See here for more information.

The function defines the differents options accepted by sshpass to input the password, which is not that useful as an alias is usually used for sshpass. The important is that the function delegates the completion for ssh (or scp) to Zsh's builtin completion for ssh.

#compdef sshpass

_sshpass() {
  local context state line
  typeset -A opt_args

  _arguments \
    '(-d -e -f -p)-d+[read password from file descriptor]:file descriptor:_file_descriptors' \
    '(-d -e -f -p)-e[take password from $SSHPASS]' \
    '(-d -e -f -p)-f+[read password from file]:file:_files' \
    '(-d -e -f -p)-p+[actual password]::' \
    '(-)1:command:->command' \
    '*::arguments: _normal' \
    && return

  case $state in
    command)
      if ((CURRENT == 2))
      then
        # insist that the first argument must be an option
        compadd -- -d -e -f -p
      else
        # complete the `command' entered (ssh, scp, etc.)
        _command_names -e
      fi
      ;;
  esac
}

_sshpass "$@"

Misc

Avoid storing commands starting by a space in the shell's history:

setopt hist_ignore_space