2022-11-22 2FA, SSH, pass, updated

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
  ...