Convenient Password/OTP SSH
Updates
- 2022-11-25
-
Mention sshpass packages;
- 2023-03-09
-
Add note on ControlMaster.
So, your site has regressed to requiring passwords for SSH login, and even
less conveniently decided to use 2FA with a TOTP (or perhaps HOTP) second
factor “because security”, regardless of how secure your public key
was.See a previous post
concerning better ways to work; Kerberos can require OTP for the initial
credential.
You may be particularly annoyed at it breaking automation.
Here’s how to have an apparently-passwordless experience in most cases, using
an appropriate password manager, and a little script. It’s Unix-only, but
something similar should work on other OSes — possibly the same thing, given
a suitably POSIX-ish environment.
There’s nothing profound in this example, but I didn’t immediately find something similar, and it’s probably useful to have something more-or-less canned. See below for the script.
It uses pass
with the
otp extension,Debian
package pass-extension-otp
.
but could be adapted to other
command-line password managers that support the relevant sort of
OTP.Or a hardware token, as below.
The script
assumes you have a pass
entry named
host/
id for your userid and the appropriate host,
containing the password and the
otpauth
URI to generate the TOTP or HOTP codes. With luck the
enrollment process provides the URI or separate parameters explicitly, and you
have it, but you may need to decode a QR code, or extract it from a software
token.For Duo, see another article.
Now you can print credentials for the target system, but you can’t just pipe
them into ssh
; ssh
requires interactive terminal (‘tty’)
input. You need to subvert that with (something like) the program
sshpass
(1), which sets up a pseudo
terminal (‘pty’) for the job.See also
expect
.
Note that you
need the modified version of sshpass
, not the original which is
likely packaged for your distribution and only deals with a password, not the
second factor. Backward-compatible Fedora/EPEL/openSUSE rpms are available
from my copr
repo, and Debian/Ubuntu dpkgs from
Open Build Service.
It’s straightforward to combine pass
and sshpass
, but you
want a convenience script, and it potentially needs to be more-or-less
system-specific for systems which prompt in different ways for the password
and second factor. You can give the script a short name appropriate for the
system, so you only need to type that name to access the default host. The
example will also accept arguments comprising a complete ssh
,
scp
, or sftp
command to use, i.e. either
$ example
or
$ example sftp fred@example.com
If you use something like rsync
, which execs an ssh
process,
there may be a way of configuring it to use your script.The
RSYNC_RSH
environment variable in rsync
’s case.
However, you can’t, as far as I know, use this sort of technique with things
(like X2Go) which use libssh
/libssh2
, or paramiko
in Python applications.
pass otp
uses GnuPG encryption, assumedly with
gpg-agent
(1)Which can also act as an SSH agent.
to avoid
always typing encryption passphrases. If you want unattended automated
ssh
along these lines, you’ll need to make sure the agent has the
relevant passphrase cached and it doesn’t expire while required. However, you
might consider your threat model and conclude you don’t actually need the
secrets encrypted on a trusted system, and store the password and OTP secrets
in the clear. See a cryptography engineer’s
‘unpopular
opinion’ of plain text secrets on a trusted system.You probably
don’t want to wire them into the script to avoid inadvertently copying it
somewhere insecure.
Of course, the same argument applies to the SSH keys
that the regression to passwords is usually about, whether or not they’re
backed by hardware. In contrast, on an untrustworthy system you might worry a
bit even about something with an encrypted store like pass
.
Thinking of hardware, you can store the secret and
generate OTPs with a hardware key (e.g. varieties of Nitrokeys and Yubikeys,
or Solo2). With Solo2, you could register the secret with the same label as
for pass
,
and then replace pass otp
with
solo2 app oath totp
to get the code from the key; with a Yubikey
ykman oath accounts code -s
does it. It is also possible to generate OTPs with a TPM, but at least with
tpm2-totp, the only
implementation I found, you can’t specify the secret, which is for device
attestation.
Anyway, the example script [raw version] is:
#!/bin/sh
# Copyright 2022 Dave Love, University of Manchester
# Licence: BSD-2-Clause <https://spdx.org/licenses/BSD-2-Clause>
# Use the modified sshpass and the pass otp extension to avoid typing
# 2-factor password and OTP, if that's required.
# Modify as appropriate
HOST=example.com # default log in to here -- changeme!
LOGIN=$USER # default user
# These are for pam_sss. For pam_duo it might be "Password:" and
# "Passcode or option"
PROMPT1="First Factor:" # password prompt
PROMPT2="Second Factor:" # prompt for an OTP
usage() {
cat <<EOF
Usage: $(basename $0) [argument]...
Run an SSH command under sshpass(1) with password and OTP value from pass(1).
Any arguments are used as the entire command (which should be an
invocation of either ssh, scp, or sftp.) Otherwise a command is
constructed of the form:
ssh -o ... $LOGIN@$HOST -- [argument]...
where -o ... specifies keyboard-interactive,password authentication.
The matched prompts for credentials are "$PROMPT1" and "$PROMPT2".
The pass "otp" extension is required, configured for the target under
the pass entry "$HOST/$LOGIN". The modified sshpass from
https://github.com/dora38/sshpass is also required.
Modify this script if any of those are inappropriate.
EOF
}
case $1 in
-h|--help) usage; exit ;;
esac
if ! sshpass --help 2>/dev/null | grep -q OTP; then
printf '"sshpass" supporting OTP not found: see
https://github.com/dora38/sshpass\n' >&2
exit 1
fi
# pass-extension-otp is packaged for Debian, at least
if ! pass otp --help >/dev/null 2>&1; then
printf '"pass" with the "otp" extension not found; see
https://github.com/tadfisher/pass-otp\n' >&2
exit 1
fi
# No command -- make one
if [ -z "$1" ]; then
set ssh -o "PreferredAuthentications keyboard-interactive,password" "$LOGIN@$HOST"
fi
# Allow a complete command supplied as args, but insert the option to
# avoid trying to use other authN. Otherwise use default login.
case $1 in
ssh|scp|sftp)
# We could just append -o ... for ssh, but not for scp or sftp
cmd=$1; shift
set "$cmd" -o "PreferredAuthentications keyboard-interactive,password" "$@" ;;
*)
set ssh -o "PreferredAuthentications keyboard-interactive,password" "$LOGIN@$HOST" -- "$@" ;;
esac
sshpass -P "$PROMPT1" -O "$PROMPT2" -d 4 -c "pass otp $HOST/$LOGIN" "$@" 4<<EOF
$(pass $HOST/$LOGIN|head -1)
EOF
Aside on ControlMaster
It may also be useful to know about the OpenSSH ControlMaster
facility. It enables you to re-use a connexion, so you only have to use 2FA
once, if you don’t use the technique above. My ~/.ssh/config
contains
this fragment (which requires a pre-created ~/.ssh/sockets/
)
Host *
...
ControlMaster auto
ControlPersist yes
ControlPath ~/.ssh/sockets/cm-%r@%h:%p
...