Multi-factor SSH authentication using YubiKey and SSH public keys together

Posted: December 18th, 2011 | Filed under: Fedora | Tags: , , , , | 11 Comments »

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…