Non-interactive SSH password authentication

Vincent Bernat

SSH of­fers sev­eral forms of au­then­ti­ca­tion, such as pass­words and pub­lic keys. The lat­ter are con­sid­ered more se­cure. How­ever, pass­word au­then­ti­ca­tion re­mains preva­lent, par­tic­u­larly with net­work devices.1

A clas­sic so­lu­tion to avoid typ­ing a pass­word for each con­nec­tion is ssh­pass, or its more cor­rect vari­ant passh. Here is a wrap­per for Zsh, get­ting the pass­word from pass, a sim­ple pass­word manager:2

pssh() {
  passh -p <(pass show network/ssh/password | head -1) ssh "$@"
}
compdef pssh=ssh

This ap­proach is a bit brit­tle as it re­quires pars­ing the out­put of the ssh com­mand to look for a pass­word prompt. More­over, if no pass­word is re­quired, the pass­word man­ager is still in­voked. Since OpenSSH 8.4, we can use SSH_ASKPASS and SSH_ASKPASS_REQUIRE instead:

ssh() {
  set -o localoptions -o localtraps
  local passname=network/ssh/password
  local helper=$(mktemp)
  trap "command rm -f $helper" EXIT INT
  > $helper <<EOF
#!$SHELL
pass show $passname | head -1
EOF
  chmod u+x $helper
  SSH_ASKPASS=$helper SSH_ASKPASS_REQUIRE=force command ssh "$@"
}

If the pass­word is in­cor­rect, we can dis­play a prompt on the sec­ond attempt:

ssh() {
  set -o localoptions -o localtraps
  local passname=network/ssh/password
  local helper=$(mktemp)
  trap "command rm -f $helper" EXIT INT
  > $helper <<EOF
#!$SHELL
if [ -k $helper ]; then
  {
    oldtty=\$(stty -g)
    trap 'stty \$oldtty < /dev/tty 2> /dev/null' EXIT INT TERM HUP
    stty -echo
    print "\rpassword: "
    read password
    printf "\n"
  } > /dev/tty < /dev/tty
  printf "%s" "\$password"
else
  pass show $passname | head -1
  chmod +t $helper
fi
EOF
  chmod u+x $helper
  SSH_ASKPASS=$helper SSH_ASKPASS_REQUIRE=force command ssh "$@"
}

A pos­si­ble im­prove­ment is to use a dif­fer­ent pass­word en­try de­pend­ing on the re­mote host:3

ssh() {
  # Grab login information
  local -A details
  details=(${=${(M)${:-"${(@f)$(command ssh -G "$@" 2>/dev/null)}"}:#(host|hostname|user) *}})
  local remote=${details[host]:-details[hostname]}
  local login=${details[user]}@${remote}

  # Get password name
  local passname
  case "$login" in
    admin@*.example.net)  passname=company1/ssh/admin ;;
    bernat@*.example.net) passname=company1/ssh/bernat ;;
    backup@*.example.net) passname=company1/ssh/backup ;;
  esac

  # No password name? Just use regular SSH
  [[ -z $passname ]] && {
    command ssh "$@"
    return $?
  }

  # Invoke SSH with the helper for SSH_ASKPASS
  # […]
}

It is also pos­si­ble to make scp in­voke our cus­tom ssh function:

scp() {
  set -o localoptions -o localtraps
  local helper=$(mktemp)
  trap "command rm -f $helper" EXIT INT
  > $helper <<EOF 
#!$SHELL
source ${(%):-%x}
ssh "\$@"
EOF
  command scp -S $helper "$@"
}

For the com­plete code, have a look at my zshrc. As an al­ter­na­tive, you can put the ssh() func­tion body into its own script file and re­place command ssh with /usr/bin/ssh to avoid an un­wanted re­cur­sive call. In this case, the scp() func­tion is not needed anymore.

Up­date (2023-12)

This post was heav­ily dis­cussed on Hacker News.