Non-interactive SSH password authentication
Vincent Bernat
SSH offers several forms of authentication, such as passwords and public keys. The latter are considered more secure. However, password authentication remains prevalent, particularly with network devices.1
A classic solution to avoid typing a password for each connection is sshpass, or its more correct variant passh. Here is a wrapper for Zsh, getting the password from pass, a simple password manager:2
pssh() { passh -p <(pass show network/ssh/password | head -1) ssh "$@" } compdef pssh=ssh
This approach is a bit brittle as it requires to parse the output of the ssh
command to look for a password prompt. Moreover, if no password is required, the
password manager is still invoked. 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 password is incorrect, we can display a prompt on the second tentative:
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 possible improvement is to use a different password entry depending on the remote 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 possible to make scp
invoke our custom 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 complete code, have a look at my zshrc. As an alternative, you can
put the ssh()
function body into its own script file and replace command ssh
with /usr/bin/ssh
to avoid an unwanted recursive call. In this case, the
scp()
function is not needed anymore.
Update (2023-12)
This post was heavily discussed on Hacker News.
-
First, some vendors make it difficult to associate an SSH key with a user. Then, many vendors do not support certificate-based authentication, making it difficult to scale. Finally, interactions between public-key authentication and finer-grained authorization methods like TACACS+ and Radius are still uncharted territory. ↩︎
-
The clear-text password never appears on the command line, in the environment, or on the disk, making it difficult for a third party without elevated privileges to capture it. On Linux, Zsh provides the password through a file descriptor. ↩︎
-
To decipher the fourth line, you may get help from
print -l
and the zshexpn(1) manual page.details
is an associative array defined from an array alternating keys and values. ↩︎