Setting up OATH 2FA Authentication on Linux

It seems like everywhere you log in these days wants you to use some kind of 2 factor (multi-factor) authentication. The most common one you will have likely seen is the OATH time based code generation where you use an authenticator app to generate codes that cycle every 30 seconds. Well, it turns out you can set this up for yourself on your own linux hosts using the pam_oath library.

First, you’ll need to install some requisite packages. I am basing this tutorial on Ubuntu 20. The same principle should work on RedHat derivatives.

apt install libpam-oath oathtool qrencode

Setting up OATH users

To set up a user account edit the file /etc/users.oath (per user files can also be used, but for the sake of the example, I am using one unified file) and add a line for each user on the system that will be using 2FA. The format of the line is:

HOTP/T30	testuser	-	26b800e7429db623ef086f4f9d46ef

The first field tells it which type of OATH passwords to use. We are setting it up for time based codes, with a 30 second window.

The second field is the user name (each user gets their own line).

Then comes a ‘-‘ (dash) for no PIN or a ‘+’ for an externally configured PIN provider (OTPAuthPINAuthProvider). I am not using a separate PIN provider here. More information can be found on the wiki page.

The hex humber is a random gibberish password. You can either create a complex password yourself, or just have the system generate it for you. As you will never have to type this, it is better to use a random password than something that is memorable or guessable. A command that will generate a nice 30 byte code is:

dd if=/dev/random bs=1M count=1 status=none | shasum | cut -b 1-30 

This simply take one megabyte of random data, generates a SHA1 checksum of it, and returns the first 30 bytes. (if the command hangs, then you do not have enough entropy in your system, and you can substitute /dev/urandom for slightly less random data, but guaranteed to return a value)

Now that we have a user created, and a password chosen, we can generate a QR code for the OATH authenticator app of your choice. The command is all one line:

qrencode --type=ANSIUTF8 otpauth://totp/testuser@technomancer.com?secret=$( oathtool --verbose --totp 26b800e7429db623ef086f4f9d46ef --digits=6 -w 1 | grep Base32 | cut -d ' ' -f 3 )\&digits=6\&issuer=technomancer.com\&period=30 

Let’s break the command down:

qrencode will generate the code. We set the type to ANSI-UTF8 terminal graphics so you can generate this in an ssh login. It can also generate other formats if you were to incorporate this into a web interface. See the man page for qrencode for more options. The rest of the line is the being encoded into the QR code, and is a URL of the type otpauth, with time based one-time passwords (totp). The user is “testuser@technomancer.com“, though PAM will ignore the @technomancer.com if you are not joined to a domain (I have not tested this with domains yet).

The parameters follow the ‘?‘, and are separated by ‘&‘.

otpauth uses a base32 hash of the secret password you created earlier. oathtool will generate the appropriate hash inside the block:

 $( oathtool --verbose --totp 26b800e7429db623ef086f4f9d46ef | grep Base32 | cut -d ' ' -f 3 )

We put the secret from earlier, and search for “Base32”. This line will contain the Base32 hash that we need from the output:

Hex secret: 26b800e7429db623ef086f4f9d46ef
Base32 secret: E24ABZ2CTW3CH3YIN5HZ2RXP
Digits: 6
Window size: 0
Step size (seconds): 30
Start time: 1970-01-01 00:00:00 UTC (0)
Current time: 2022-03-03 00:09:08 UTC (1646266148)
Counter: 0x3455592 (54875538)

368784 

From there we cut out the third field, “E24ABZ2CTW3CH3YIN5HZ2RXP“, and place it in the line.

Next, we set the number of digits for the codes to be 6 digits (valid values are 6, 7, and 8). 6 is sufficient for most people, and easier to remember.

The issuer is optional, but useful to differentiate where the code came from.

We set the time period (in seconds) for how long a code is valid to 30 seconds. Note: Google authenticator ignores this and uses 30 seconds whether you like it or not.

This will give output a QR code in your terminal that you can scan in to your app.

Setting up SSH

Now set up OATH to work with ssh, and it assumes you have a working OpenSSH server process running, and that the user accounts have an authorized_keys file with working keys.

First we are going to configure sshd to work with PAM. Edit /etc/ssh/sshd_config and set the following values (you may need to change or add lines, depending on your current set up):

PasswordAuthentication no
ChallengeResponseAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

We disable plain password authentication, as we want PAM to handle the authentication for us rather than ssh doing a simple password check. Then we turn on a challenge/response authentication method so that PAM can query us for values.

AuthenticationMethods is a trickier directive. You can configure here whatever security you like. The key is that if you have comma separated values, ALL of the methods specified must pass. If you have space separated methods, then ANY of them will pass. They can be combined as well. See the sshd_config man page for more details and set to your liking. For this example, however, I am going with the “something you have” (the ssh key), and “something you know”, the 2FA password. This method requires you have a public key set up in authorized_keys, and will then ask you for the account password and a 2FA code. The keyboard-interactive method passes the authentication method off to PAM, and the interaction you get from there is taken from the pam_oath plugin.

If you prefer to allow the key to work without a prompt, but still use 2FA if the user does not have a key, then change the comma in “publickey,keyboard-interactive” to a space.

You can also use Match directives to enforce 2FA under certain conditions, but not under others. For example, if you didn’t want to be bothered with it while you are logging in on your LAN, but do from any other network, you could add something like:

Match Address 127.0.0.1,10.0.0.0/8,192.168.0.0/24
    Authenticationmethods publickey

You can create whatever combination of Match characteristics and AuthenticationMethods to be as secure (or insecure) as you like. Just be sure to test them and make sure it works as intended. See the sshd_config manpage for more information.

Note: There appears to be a bug in Ubuntu 20’s ssh implementation, and sub-configs put in /etc/ssh/sshd_config.d do not work as expected. I have done a lot of testing, and it reads the files, and even matches the rules, but does not perform the directives under the match. If the exact same lines are added to /etc/ssh/sshd_config, they work fine. In the forums, I have found a number of other people seeing the same issue. But no action seems to have been taken to fix it, so for now, you need to put your Match rules in /etc/ssh/sshd_config rather than in sub-config files.

Don’t forget to restart sshd:

systemctl restart sshd

Setting up PAM

Quick Tip: Before you edit or change any file related to PAM, make sure you have a root shell opened; preferably on the console (if available). You can very easily lock yourself out if you make a mistake. Trust me. And remember, when you make a change to a PAM file, unlike sshd, it takes effect the moment you save it.

This tutorial is specific to ssh, but you can use it under any module that PAM supports using the auth mechanism. Just be careful you don’t inadvertently lock yourself out of root, otherwise you’ll have to boot from a rescue disk to get back in.

Add this line to /etc/pam.d/sshd under the line that reads “@include common-account“:

auth requisite pam_oath.so usersfile=/etc/users.oath window=20 digits=6

That means that it will be required for a user to log in, and will use the file /etc/users.oath to manage users and their keys. Per-user files can also be created and stored in user home directories.

The window is how many passwords an attempt will look ahead; so in this case it would see 20 “future” passwords. This allows a 10 minute window to get in, and will also allow for some clock skew on the servers. The range can be 5-50.

Make sure the number of digits you specify here agrees with the value you set when configuring user passwords and the QR code.

If you do not want to have all users required to use 2FA, you can tell PAM to only ask for a one time password if they are in a specific group, in this example, the “oath” group:

auth [default=1 success=ignore] pam_succeed_if.so quiet user ingroup oath
auth requisite pam_oath.so usersfile=/etc/users.oath window=20 digits=6

Supposedly, you can also set the pam_oath plugin to store the passwords in some other location, like each user’s home directory, but I cannot (yet) get it to work.

The pam_oath manpage goes in to more detail, but the relevant bit is:

"usersfile": Specify filename where credentials are stored, for
             example "/etc/users.oath". The placeholder values
             "${USER}" and "${HOME}" may be used to specify the
             filename on a per-user basis. The values "${USER}" and
             "${HOME}" expand to the user's username and home
             directory, respectively.

In doing this, you can allow users to set/reset their own keys. If you do get it working, this file must be treated like the private ssh key; only root or the owner should be able to read or write to this file.

Bringing It All Together

To make my life easier when setting up users for OATH, I made a script to bring it all home. It assumes you are using the example above to only have users in the oath group use 2FA. If the user does not already have an entry in /etc/users.oath, then a random password will be generated and added. If the user already has an entry, it will be left alone. It will then print the URL, and a QR code on the terminal. It also generates a PNG graphic and saves it as username-oath.png that you can give to the user. Make sure you delete this after giving it to the user.

#!/bin/bash

#
# The original goal of this was to have a SUID script to allow users
# to add themselves to OATH, and generate their QR code, but it seems
# that Ubuntu doesn't elevate privileges for SUID scripts anymore.
# I will figure that out later.
# In the meantime, this script does make it much easier for the admin
# to add a user to 2FA and to generate their QR code.
#
# OATH set up script
# Scott Garrett
#
# Version: 2022030701
#

# You must be root to use this script.

if [ $(whoami) != "root" ]
then
        echo "This script must be run as root."
        exit 1
fi

if [ ! "$1" ]
then
	echo "Please specify the username."
	exit 1
fi

OATHSECRETFILE=/etc/users.oath

OATHUSER=$1
OATHGROUP=oath

function printoath
{
	OATHURL="otpauth://totp/${OATHUSER}@technomancer.com?secret=$( oathtool --verbose --totp "${OATHCODE}" --digits=6 -w 1 | grep Base32 | cut -d ' ' -f 3 )&digits=6&issuer=technomancer.com&period=30"

	echo ${OATHURL}

	qrencode --type=ANSIUTF8 ${OATHURL}

# This will generate a PNG file of the QR code that can be given to the user
# if they are not near you or the console.  Make sure you remove this after
# giving it to the user, and encourage them to delete the file after as well.

	qrencode --type=PNG -o "${OATHUSER}-oath.png" ${OATHURL}
}

if [ ! "$( getent passwd ${OATHUSER} )" ]
then
	echo "User ${OATHUSER} does not exist."
	exit 1
fi


if [ ! "$( getent group ${OATHGROUP} )" ]
then
	echo "Group ${OATHGROUP} does not exist."
	exit 1
fi

# If there is no secrets file, then create one and set the permissions
# so only root can red or write to the file
if [ ! -f "${OATHSECRETFILE}" ]
then
	touch "${OATHSECRETFILE}"
	chown root "${OATHSECRETFILE}"
	chmod 0600 "${OATHSECRETFILE}"
fi

# Check to see if there is a code already for the user.
OATHCODE=$( cat ${OATHSECRETFILE} | awk -v OATHUSER="${OATHUSER}" '{ if ( $2 == OATHUSER ) print $4; }' )

# If there is a code, then print the otpauth URL, and a QR code to go with it.
if [ "${OATHCODE}" ]
then
	printoath
else
# If there isn't a code for the existing user, then create one and add it to the
# users.oath file.
	echo "User ${OATHUSER} not in ${OATHSECRETFILE}...Creating new user."
	OATHCODE=$( dd if=/dev/random bs=1M count=1 status=none | shasum | cut -b 1-30 )

	echo "HOTP/T30	${OATHUSER} - ${OATHCODE}" >> "${OATHSECRETFILE}"

	printoath

	echo "Adding ${OATHUSER} to oath group."
	usermod -a -G ${OATHGROUP} ${OATHUSER}
fi

2 thoughts on “Setting up OATH 2FA Authentication on Linux”

  1. “Then, for no reason I can find, a whitespace separated dash (‘-‘).”

    The pam-oath manpage that you reference contains a link to the /etc/usrs.oath format documentation. Here it states that for this third field of an entry ‘-‘ signifies the absence of a PIN, an integer string defines a PIN, and ‘+’ indicates an external PIN generator is used. The PIN must then be prefixed to the TOPT.

    Reply
    • Thanks for that! I think I was tired when I was writing that up (that’s my story, I’m sticking to it). I have updated to include the description and the reference.

      Reply

Leave a Reply to Scott Garrett Cancel reply