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

Posted: December 18th, 2011 | Author: | Filed under: Fedora | Tags: , , , , | 10 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…

10 Comments

Matt Domsch said at 3:49 am on December 19th, 2011:

strictly speaking, you didn’t need 2 yubikeys. Each yubikey allows for up to two different configurations to be stored in them. The way Fedora Infrastructure tends to set up keys, the first position is for FI’s authenticator; the second is for the yubikey shared authenticator. You hold the button longer to access the second key.

Also, talk to skvidal, because he’s been hunting this down for FI’s use for sysadmin-main, rel-eng, or other highly sensitive groups. I think he’s planning a 2fa hackfest at the upcoming FUDCon Blacksburg.

Daniel Berrange said at 3:55 pm on December 19th, 2011:

It appears that some people are working on patches to OpenSSH to allow an administrator to enable multiple required authentication methods at once.

https://bugzilla.mindrot.org/show_bug.cgi?id=983

Once this bug is resolved, we’ll be able to configure YubiKey via PAM as also use SSH public keys at the same time, without resorting to my nasty ForceCommand hack.

Brandon Thomson said at 4:02 pm on February 4th, 2013:

Thanks very much; this works brilliantly!

I skipped the line with:
Match User fred
assuming that it would apply to all logins, and that seems to be the case.

I don’t understand how the custom API key improves security… is there a ref for that?

wernert said at 8:10 pm on April 16th, 2013:

The method that you describe, does it need internet access to validate the key at the yubico server?

(I’ve set up the Yubico Virtual Appliciance which is stand alone. I’d like the same setup in Debian.)

Daniel Berrange said at 9:10 pm on April 16th, 2013:

Yes, the setup I did relies on Yubico’s primary auth servers. You can setup it up to use a private server as desired, but I’ve not investigated that

Jan said at 10:17 pm on July 2nd, 2013:

I think this will get much easier with openssh 6.2:

http://lwn.net/Articles/544640/

“6.2 adds a new keyword, AuthenticationMethods, which takes one or more comma-separated lists of authentication methods as its argument (with the lists themselves separated by spaces). If only one list is supplied, users must successfully complete all of the methods—in the order listed—to be granted access. If several lists are provided, users need complete only one of the lists.”

One could use “AuthenticationMethods publickey,keyboard-interactive:pam” and let pam handle the yubikey stuff.

Steve said at 5:52 pm on April 7th, 2015:

Should this also work with scp? when I deploy the shell script, and try to login, it just hangs after giving the UNIX password. I am never prompted for my yubikey.

I do have it working for ssh.

Daniel Berrange said at 9:10 am on April 8th, 2015:

You need an alias for it to work with scp eg

alias scp-otp=’echo -n “Please provide Yubi OTP: “; read -s SCP_AUTH_OTP; echo “”; echo “Yubikey entered”;export SCP_AUTH_OTP;scp -oSendEnv=SCP_AUTH_OTP’

Mauricio Terrats said at 11:07 pm on May 25th, 2015:

Thanks for this jewel. I have only 1 thing I’d change:

exec `grep “^$(whoami)” /etc/passwd | cut -d “:” -f 7`

In my specific case, that only outputs /bin/bash which is executed but the output is not sent to the screen. To fix this I commented that line and just added:

/bin/bash -l

This creates a bash session as if it was invoked and loading .bash_profile

Mauricio Terrats said at 12:06 am on May 26th, 2015:

I’d like to contribute using my super skills to make this script almost perfect, adding a new array containing the cloud servers from yubico, so it will grab one of the list, so if any goes down, you can try again and you should get a new one. It is random, not perfect, but it will offer more availability than having only 1. Forgive my code:

YUBICO_URLS=(api.yubico.com api2.yubico.com api3.yubico.com api4.yubico.com api5.yubico.com)
YUBICO_URLS_NUM=${#YUBICO_URLS[*]}
YUBICO_URL=${YUBICO_URLS[$((RANDOM%YUBICO_URLS_NUM))]}

Leave a Reply





Spam protection: Sum of thr33 plus f0ur ?: