Two-factor SSH authentication with U2F hardware security key

Standard

I’m shocked at the lack of documentation on using a U2F security key for two-factor SSH authentication. Luckily, it isn’t too difficult, thanks to the fact that Yubico has done a lot of the hard work for us.

A prerequisite for this guide is compiling and installing the pam_u2f.so module. Luckily, there is a fair amount of documentation online on how to do this. You’ll also need a system that can run the u2f-host command. Instructions for getting set with these are here and here, respectively. You will need both of these for both the server andclient.

If you are working with a remote system which you do not have physical access to, I recommend making sure you have a backup SSH session open at all times, as failings in the SSHd or the PAM configuration can cause you to be locked out. You are warned.

Requirements
First, come up with a unique name for the system you wish to SSH in to. This doesn’t have to be wholly unique, just make sure it doesn’t collide with anything else on the key, really.
For this example, we’ll use backupserver.
You’ll also need to know which account you wish to secure. For this example, we’ll use root.
For this tutorial, I used a MacOS X system to generate the key and responses, and secured a CentOS 7 system. Instructions should be easily adaptable to other operating systems.

Registering the key
We now need to register a new key with your U2F device. In order to do this, we’ll use pamu2fcfg.

On the system with the U2F device connected, run the following command:
pamu2cfg -o pam://backupserver -i pam://backupserver -u root
Make sure to change ‘backupserver’ and ‘root’ to what you decided earlier.
Your U2F device should then start flashing. Touch it to continue.
After you’ve touched the device, you’ll be left with something similar to this on the command line:
root:cCaQUJyqmU68z6KVAdpyk2871wuRVtgDbXCKq5A8zqBj577vdUhdLKeOQG4l9yG-t7Ze8wgtsdF7l0GEjO0g-A,04f32cb3dd0b0b1ac20517050ab1dd2700f410f638675c7ebe18b9607459171c8b82de6d1a1d5d25429157db43943033b741c0c376c375ce460628bd7464677fb4
This is the U2F key definition. The first part is the username the key is for, and the second part contains both the key handle and the user key. These are seperated by commas.

Configuring the U2F definition
Now login to the server you are wanting to protect, and execute the following command, making sure to replace the data with the output you got from the above command (in quotes). Example:
echo 'root:cCaQUJyqmU68z6KVAdpyk2871wuRVtgDbXCKq5A8zqBj577vdUhdLKeOQG4l9yG-t7Ze8wgtsdF7l0GEjO0g-A,04f32cb3dd0b0b1ac20517050ab1dd2700f410f638675c7ebe18b9607459171c8b82de6d1a1d5d25429157db43943033b741c0c376c375ce460628bd7464677fb4' > /etc/u2f_mappings

This will create a file which we’ll use to tell the PAM module to find your key information.

Configuring SSH
Next, let’s prepare the SSHd configuration. We have to ensure to turn on ChallengeResponseAuthentication and we must make sure SSHd is set to use PAM.
Open up /etc/ssh/sshd_config using your favourite editor.
Make sure to set the following:

ChallengeResponseAuthentication yes

It is probably already defined, so search for it and set it to Yes, or uncomment it.
And also make sure UsePAM is set to Yes.

UsePAM Yes

Now reload SSHd:

systemctl reload sshd

Configuring PAM
Next we need to modify the PAM SSH configuration. Open up /etc/pam.d/sshd. It should look a bit like this:

#%PAM-1.0
auth required pam_sepermit.so
auth substack password-auth
auth include postlogin
account required pam_nologin.so
account include password-auth
password include password-auth
# pam_selinux.so close should be the first session rule
session required pam_selinux.so close
session required pam_loginuid.so
# pam_selinux.so open should only be followed by sessions to be executed in the user context
session required pam_selinux.so open env_params
session optional pam_keyinit.so force revoke
session include password-auth
session include postlogin

Under:

auth substack password-auth

Add the following, replacing backupserver with the name you chose above:

auth required pam_u2f.so origin=pam://backupserver appid=pam://backupserver authfile=/etc/u2f_mappings manual

This configuration will ensure that the security key challenge-response is done after the password authentication.

Testing and Logging In
Now, it’s time to test this configuration. Log in to the server in a new session to test if it works.
After you supply your password, you should be met with the following prompt.

Now please copy-paste the below challenge(s) to 'u2f-host -aauthenticate -o pam://backupserver'
{ "keyHandle": "Rr3ZPpbO6fuCF1_w1RgxY2Ft6FpZ3CtABHgeySshQpH470dbVlZCbFIjUEzut3JxQrCoUWJfo4YKjAxJxL2MwA", "version": "U2F_V2", "challenge": "zwkjaDcNdfjMSqOKIq84ZsF9MlS8M0P3rmHCB7PSuKo", "appId": "pam:\/\/backupserver" }
Now, please enter the response(s) below, one per line.
[0]:

On your system, run the following — replacing the u2f-host command and challenge with what you are asked for:

echo '{ "keyHandle": "Rr3ZPpbO6fuCF1_w1RgxY2Ft6FpZ3CtABHgeySshQpH470dbVlZCbFIjUEzut3JxQrCoUWJfo4YKjAxJxL2MwA", "version": "U2F_V2", "challenge": "zwkjaDcNdfjMSqOKIq84ZsF9MlS8M0P3rmHCB7PSuKo", "appId": "pam:\/\/backupserver" }' | u2f-host -aauthenticate -o pam://backupserver

You should get a response like this:

{ "signatureData": "AQAAADwwRQIhANLPROgaGa5NYrTBY8qK8bCuy3Vc_LI6wZdrFhquyt0lAiA4ppzdGv277g853EW1TKJMC788TwxWV4SOPUPltDGyYA==", "clientData": "eyAiY2hhbGxlbmdlIjogIm1DTDBxel9aV1NvR1pHcElSZk1GYm5WSkcyNHBGMG1tTjR4Z2s4Z3VIU3MiLCAib3JpZ2luIjogInBhbTpcL1wvYmFja3Vwc2VydmVyIiwgInR5cCI6ICJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiB9", "keyHandle": "Rr3ZPpbO6fuCF1_w1RgxY2Ft6FpZ3CtABHgeySshQpH470dbVlZCbFIjUEzut3JxQrCoUWJfo4YKjAxJxL2MwA" }

Paste that into your SSH session, and press Enter. Provided you supplied a correct password and challenge response, you should be logged in.