Multi-factor SSH authentication using YubiKey and SSH public keys together
UPDATE: the setup described here is flawed because it only correctly secures the primary SSH channel. ie if you use port redirection like ‘ssh -L 80:localhost:80 example.com’ then the shell session will require you to enter the yubikey code, but the port redirect will be activated and usable prior to you entering the yubikey. I’d thus strongly recommend NOT FOLLOWING the instructions in this blog post, and instead upgrade to OpenSSH >= 6.2 which has proper built-in support for multi-factor authentication, avoiding the need for this hack.
A month or two ago I purchased a couple of YubiKey USB tokens, one for authentication with Fedora infrastructure and the other for authentication of my personal servers. The reason I need two separate tokens is that Fedora uses its own YubiKey authentication server, thus requiring that you burn a new secret key into the token. For my personal servers I decided that I would simply authenticate against the central YubiKey authentication server hosted by YubiCo themselves. While some people might not be happy trusting a 3rd party service for authentication of their servers, I decided this was not a big problem since I intend to combine the YubiKey authentication with the existing strong SSH RSA public key authentication which is entirely under my control.
YubiKey authentication via PAM
To start off with I decided to follow a well documented configuration path, enabling YubiKey authentication for SSH via PAM. This was pretty straightforward and worked first time. The configuration steps were
- Build and install the yubico-pam module. You might be lucky and find your distro already ships packages for this, but I was doing this on my Debian Lenny server which did not appear to have any pre-built PAM module.
- Create a file /etc/yubikey_mappings which contains a list of usernames and their associated yubikey token IDs. The Token ID is the first 12 characters of a OTP generated from a keypress of the token. Multiple token IDs can be listed for each user.
$ cat > /etc/yubikey_mappings <<EOF fred:cccccatsdogs:ccccdogscats EOF
- Get a unique API key and secret for personal use from https://upgrade.yubico.com/getapikey/
- Add the yubico-pam module to the SSHD PAM configuration module using the previously obtained API key ID in place of
XXXX
$ cat /etc/pam.d/sshd # PAM configuration for the Secure Shell service # Read environment variables from /etc/environment and # /etc/security/pam_env.conf. auth required pam_env.so # [1] # In Debian 4.0 (etch), locale-related environment variables were moved to # /etc/default/locale, so read that as well. auth required pam_env.so envfile=/etc/default/locale auth sufficient pam_yubico.so id=XXXX authfile=/etc/yubikey_mappings # Standard Un*x authentication. @include common-auth ...snip...
This all worked fine, with one exception, if I had an authorized SSH public key then SSH would skip straight over the PAM “auth” phase. This is not what I wanted, since my intention was to use YubiKey and SSH public keys for login. The yubico-pam website has instructions for setting up two-factor authentication but this only works if both your factors are configured via PAM. SSH public key authentication is completely outside the realm of PAM. AFAICT from a bit of googling, it is not possible to configure OpenSSH to require PAM and public key authentication together; it considers either one of them to be sufficient on their own.
After a little more googling though, I came across an interesting hack utilizing the ForceCommand
configuration parameter of SSHD. The gist of the idea is that instead of configuring YubiKey authentication via PAM, you use the ForceCommand
parameter to get SSHD to invoke a helper script which performs a YubiKey authentication check and only then executes the real command (ie login shell).
I made a few modifications to Alexandre’s script mentioned in the blog post just linked
- Use the same configuration file, /etc/yubimap_mappings, as used for centralized yubico-pam setup
- Allow the verbose debugging information to be turned off
- Load the API key ID from /etc/yubikey_shell instead of requiring editing of the helper script itself
Usage of the script is quite simple
- Create /etc/yubikey_shell containing
$ cat /etc/yubikey_shell # Configuration for /sbin/yubikey_shell # Replace XXXX with your 4 digit API key ID as obtained # from https://upgrade.yubico.com/getapikey/ YUBICO_API_ID="XXXX" # Change to 1 to enable debug logs for troubleshooting login #DEBUG=1 # To override stanard key mapping location. This file # should contain 1 or more lines like # # USERNAME:YUBI_KEY_ID:YUBI_KEY_ID:... # # This is the same syntax used for yubico-pam #TRUSTED_KEYS_FILE=/etc/yubikey_mappings
- Create the /etc/yubikey_mappings file, if not already present from a previous yubico-pam setup
$ cat /etc/yubikey_mappings fred:cccccatsdogs:ccccdogscats
- Append to the /etc/ssh/sshd_config file a directive to enable YubiKey auth for selected users
Match User fred ForceCommand /sbin/yubikey_shell
- Save the wrapper script itself to /sbin/yubikey_shell
x DEBUG=0 TRUSTED_KEYS_FILE=/etc/yubikey_mappings # This default works, but you really want to use your # own ID for greater security YUBICO_API_ID=16 test -f /etc/yubikey_shell && source /etc/yubikey_shell STD="\\033[0;39m" OK="\\033[1;32m[i]$STD" ERR="\\033[1;31m[e]$STD" ################################################## ## Disconnect clients trying to exit the script ## ################################################## trap disconnect INT disconnect() { sleep 1 kill -9 $PPID exit 1 } debug() { if test "$DEBUG" = 1 ; then echo -e "$@" fi } if test -z "$USER" then debug "$ERR USER environment variable is not set" > /dev/stderr disconnect fi #################################### ## Get user-trusted yubikeys list ## #################################### if [ ! -f $TRUSTED_KEYS_FILE ] then debug "$ERR Unable to find trusted keys list" > /dev/stderr disconnect fi TRUSTED_KEYS=`grep "${USER}:" $TRUSTED_KEYS_FILE | sed -e "s/${USER}://" | sed -e 's/:/\n/g'` for k in $TRUSTED_KEYS do debug "$OK Possible key '$k'" done ####################################### ## Get the actual OTP ## ####################################### echo -n "Please provide Yubi OTP: " read -s OTP echo KEY_ID=${OTP:0:12} ####################################### ## Iterate through trusted keys list ## ####################################### for trusted in ${TRUSTED_KEYS[@]} do if test "$KEY_ID" = "$trusted" then debug "$OK Found key in $TRUSTED_KEYS_FILE - validating OTP now ..." if wget "https://api.yubico.com/wsapi/verify?id=$YUBICO_API_ID&otp=$OTP" -O - 2> /dev/null | grep "status=OK" > /dev/null then debug "$OK OTP validated" if test -z "$SSH_ORIGINAL_COMMAND" then exec `grep "^$(whoami)" /etc/passwd | cut -d ":" -f 7` else exec "$SSH_ORIGINAL_COMMAND" fi debug "$ERR failed to execute shell / command" > /dev/stderr disconnect else debug "$ERR Unable to validate generated OTP" > /dev/stderr disconnect fi fi done debug "$ERR Key not trusted" > /dev/stderr disconnect
The avoid the need to cut+paste, here are links to the full script and the configuration file.
After restarting the SSHD service, all was working nicely. Authentication now requires a combination of a valid SSH public key and a valid YubiKey token. Alternatively, if SSH public keys are not in use for a user, authentication will require the login password and a valid YubiKey token.
I still feel a little dirty about having to use the ForceCommand hack though, because it means yubikey auth failures don’t appear in your audit logs – as far as SSHD is concerned everything was successful. It would nice to be able to figure out how to make OpenSSH properly combine SSH public key and PAM for authentication…