{ ^_^ } sinustrom Solving life, one problem at a time!

OATH based two-factor authentication for SSH

Author: Zoltan Puskas
Categories: linux security

Setting up two-factor authentication can significantly reduce the likelihood of successful attacks against user accounts, since it makes obtaining (e.g. via phishing or guessing) the username and password insufficient to gain access. Additionally it also helps reducing replay attacks, as most tokens generate a new code for every login. One such critical authentication channel to harden is your secure shell access (SSH) to your server.

Setup properties

This two-factor authentication setup has the following properties:

  • intended for low count of machines,
  • intended for a moderate amount of users,
  • operates in a self contained manner (a.k.a. no reliance on external services),
  • uses wildly available physical or soft tokens,
  • uses HMAC based OATH-HOTP or OATH-TOTP,
  • can support multiple tokens per user,
  • applies only to remote access (e.g. not for local shell via keyboard),
  • and is easy to setup.

I will be using a Gentoo box to demonstrate the setup, but naturally this can be ported to practically any Linux distribution. If you are making these changes to a remote machine, make sure you have a reliable connection and an emergency SSH session opened on the side just in case!

Configuring SSH for 2fac

OATH toolkit

You will need to install oath-toolkit on your box where you want to enable second factor for your SSH logins. This is a piece of software that provides components and commands to deal with OATH based one-time password authentication systems. Make sure oath-toolkit is compiled with PAM support, in Gentoo do this by setting the pam use flag.

# echo "sys-auth/oath-toolkit pam" >> /etc/portage/package.use
# emerge oath-toolkit

Setting up token secrets

We want to setup second factor secrets before enabling authentication, otherwise we might end up locking ourselves out of the system if accessed by remote means.

Oath-toolkit will take a single secrets file for all the user tokens. In my setup I named it as shadow.oath and placed it into /etc. We also need it to be owned and accessible only by root, since we don’t want users looking at other people’s secrets.

# touch /etc/shadow.oath
# chown root:root /etc/shadow.oath
# chmod 600 /etc/shadow.oath

Now let’s add a secret for our user into /etc/shadow.oath. First let’s generate a secret to use:

$ head -c1024 /dev/urandom | sha512sum | cut -b 1-40
$ shuf -i 1-10000 -n 1
$ head -c1024 /dev/urandom | sha512sum | cut -b 1-20

Note that above demo secrets are 20 and 10 bytes long (represented in hexadecimal form with 40 and 20 characters). It’s possible, and recommended, to increase the length of your secret by increasing the number of characters cut from the SHA512 hash, however there are limitations:

  • oath-toolkit will only accept secrets up to 32 bytes in length,
  • Yubikeys will only accept secrets up to 20 bytes in length.

Use your favourite editor to add a line using the format described in the UsersFile specification. Example contents of /etc/shadow.oath (Do not use these values in production!):

HOTP    alice   -   cca567e2d29ec84c337bbfc82121c920ba0a2230    2428
HOTP/T30/8  alice   -   b43a663bc5cbac8b5a9b

In this example the user has two tokens assigned to their account, one time based (e.g. for a phone application) and one event based (e.g. for use with Yubikeys). Naturally further users or tokens can be added.

Warning: it’s highly recommended to set up your tokens at this time (see below) before proceeding further with the setup if you are doing this remotely!


The PAM configuration needs to be adjusted to enable authenticating via the second factor using oath-toolkit. In this setup we will only enforce second factor authentication only for SSH logins, while keeping local shell access to be username and password only, as before.

If you are not familiar with PAM configuration, it’s highly recommended to browse their documentation a bit, or at least the pam(8) man pages.

Since PAM configuration files also allow for includes, I’ve followed the scheme laid out by Gentoo when updating the PAM configuration files in /etc/pam.d. The following modifications are needed:

First let’s create /etc/pam.d/system-auth-oath with the following contents:

auth    required    pam_env.so
auth    required    pam_oath.so usersfile=/etc/shadow.oath window=20 digits=8
auth    optional    pam_permit.so

Here we say that OATH authentication is a required step, with parameters pointing to the previously created secrets file, a window setting of 20 as search depth for event based tokens, and 8 digits for one-time passwords. For more details on the parameters see the pam_oath documentation.

Above file will be included in /etc/pam.d/system-login-oath. Let’s create this file by copying /etc/pam.d/system-login and updating the auth line to use system-auth-oath.

# cp /etc/pam.d/system-login /etc/pam.d/system-login-oath
# vim /etc/pam.d/system-login-oath
# diff -u1 /etc/pam.d/system-login /etc/pam.d/system-login-oath
--- /etc/pam.d/system-login     2013-11-15 22:24:11.547236286 +0200
+++ /etc/pam.d/system-login-oath        2014-04-23 09:03:21.647565878 +0200
@@ -3,3 +3,3 @@
 auth           required        pam_nologin.so
-auth           include         system-auth
+auth           include         system-auth-oath
 account                required        pam_access.so

Now we will do a similar step to create /etc/pam.d/system-remote-login-oath which will include /etc/pam.d/system-login-oath and update the auth line again:

# cp /etc/pam.d/system-remote-login /etc/pam.d/system-remote-login-oath
# vim /etc/pam.d/system-remote-login-oath
# diff -u1 /etc/pam.d/system-remote-login /etc/pam.d/system-remote-login-oath
--- /etc/pam.d/system-remote-login      2013-11-15 22:24:11.723849435 +0200
+++ /etc/pam.d/system-remote-login-oath 2014-04-23 09:10:47.072411219 +0200
@@ -1,2 +1,2 @@
-auth           include         system-login
+auth           include         system-login-oath
 account                include         system-login

Finally in /etc/pam.d/sshd the auth line needs to be updated:

# cp /etc/pam.d/sshd /etc/pam.d/sshd.bak
# vim /etc/pam.d/sshd
diff -u1 /etc/pam.d/sshd.bak /etc/pam.d/sshd
--- /etc/pam.d/sshd.bak 2014-04-23 09:19:36.749037130 +0200
+++ /etc/pam.d/sshd     2014-04-23 09:20:19.996750057 +0200
@@ -1,2 +1,2 @@
-auth       include     system-remote-login
+auth       include     system-remote-login-oath
 account    include     system-remote-login


Once we have the proper PAM configuration, now we need to tell SSH to start using two factor authentication. For that we need to modify /etc/ssh/sshd_config. Make sure the following configurations are set:

UsePAM yes
AuthenticationMethods publickey,keyboard-interactive:pam

Note that here the important part is to append keyboard-interative:pam to the list of AuthenticationMethods option. Your configuration might differ, e.g. if you have password instead of publickey. In the above configuration the SSH server will ask for an SSH key first (e.g. an ECDSA or RSA key) and then for the one time password digits.

Restart your SSH server.

# /etc/init.d/sshd restart

Configuring tokens

Physical tokens

Though there are quite a few different physical tokens available, I will be using the Yubikey as an example since not only I’ve been using them for a long time, but they seem to produce a reliable, secure, and rugged key which doesn’t need any special hardware (e.g. smartcard reader).

To set up the Yubikey we will need the yubikey-personalization-gui application.

# emerge yubikey-personalization-gui
$ yubikey-personalization-gui

Make sure only one Yubikey is plugged into your system. After opening the application select the OATH-HOTP tab and click on the advanced button. Adjust the following fields:

  • Pick a slot to configure (in this case slot 1),
  • disable OATH token identifier,
  • adjust your HOTP length (in this case 8 digits),
  • set your moving factor seed to what we generated above by selecting fixed and entering the value,
  • set the secret key to the 20 bytes we generated before,
  • and optionally you might enable configuration protection for your key.

Finally write the configuration by clicking the button at the bottom.

Configuring your new Yubikey
Configuring your new Yubikey

Soft tokens

There is a large number of soft token applications available for all phone platforms. For the purpose of this setup I’ll be using the open source FreeOTP for Android (also available for iOS).

Note that smartphone soft tokens require the secret to be provided in Base32 form! So let’s convert ours:

$ echo cca567e2d29ec84c337bbfc82121c920ba0a2230 | xxd -r -p | base32
$ echo b43a663bc5cbac8b5a9b | xxd -r -p | base32

Fill out username, description, secret, and digits fields, select OTP type, keep algorithm on SHA1 and interval at 30 for TOTP or enter initial counter state for HOTP (if using soft token instead of Yubikey), and tap Add to save secret to the app. Tap on secret to get current token to be used for login.

Add HOTP token to FreeOTP Add TOTP token to FreeOTP Genertate a HOTP code in FreeOTP
Setting up FreeOTP with tokens

Backup token

It is advised to have a backup token set up in case the Yubikey or phone are lost or broken. To do so add another secret as described above and either initialize a backup Yubikey, or save the secret to a file on an encrypted pen drive and then use oathtool(1) to generate backup login tokens.

Example HOTP backup secret in file (e.g. alice-backup):

HOTP alice - 6c55154a3b78f0ec6482deca7cdf7281f001ece0 654

Simple genkey.sh script to generate new OTP and update backup file:


if [[ $# -lt 1 ]]; then
    echo "No keyname specified"
    exit 1

USER=$(cat $1 | cut -d' ' -f2)
KEY=$(cat $1 | cut -d' ' -f4)
CNT=$(cat $1 | cut -d' ' -f5)

oathtool -c $CNT -d 8 $KEY
CNT=$((CNT + 1))

echo "HOTP ${USER} - ${KEY} ${CNT}" > $1

Now let’s generate a backup token and see our backup file updated for next time:

$ ./genkey.sh alice-backup
$ cat alice-backup
HOTP alice - 6c55154a3b78f0ec6482deca7cdf7281f001ece0 655

Keep your backup token in a separate, offline, and secure place!


Now if everything is correct, our future SSH logins should be two factor authentication protected. Let’s try it by opening a new terminal and trying to log into the host. When asked for second factor touch your Yubikey or enter the code from your phone. We should have now our secure shell access to the box.

alice@laptop ~ $ ssh alice@secure-host
Host key fingerprint is SHA256:cJ6OKf/Qd64ceDg5XivVBVt0pJs7+GRr77aVwLqhaiU=
One-time password (OATH) for `alice':
alice@secure-host ~ $

Let’s try again, but this time entering some random numbers into the second factor prompt, and see our access being denied.

alice@laptop ~ $ ssh alice@secure-host
Host key fingerprint is SHA256:cJ6OKf/Qd64ceDg5XivVBVt0pJs7+GRr77aVwLqhaiU=
One-time password (OATH) for `alice':
One-time password (OATH) for `alice':
One-time password (OATH) for `alice':
alice@secure-host: Permission denied (keyboard-interactive).
alice@laptop ~ $

Grand, now our SSH login is two factor protected!