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…
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.
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.
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?
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.)
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
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.
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.
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’
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
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))]}
The Yubikey really rocks.
But you can also use the Yubikey Neo in other modes.
You can use ykchalresp to initialize the yubikey in challenge-response mode.
This will also work offline.
You can use the Neo as a PGP device.
Or you can initialize the yubikey to be used with your own backend service like privacyIDEA.