Enabling MFA for SSH with Puppet_

🇺🇦 Resources to help support the people of Ukraine. 🇺🇦
September 15, 2018 @16:40

For a while now I've used a Yubikey Neo as a PIV card to authenticate to my public facing hosts. This is fairly straightforward but requires a host with OpenSC on it. In my .profile I have a function called add_smartcard which will add the PIV driver to the ssh-agent. This means I actually authenticate with the key that was generated in the Yubikey and not my password.

Yubikey in my laptop

# add_smartcard - Add the PIV key to the current ssh-agent if available.
# Requires a opensc compatible smartcard and associated libs and binaries.
    # Set to a string in the opensc-tool(1) -l output for your card.
    local _card_name="Yubikey"

    # set to the installed location of the opensc libraries.
    # on OSX with HomeBrew this is /usr/local/lib
    local _lib_dir="/usr/local/lib"

    if ! quiet_which opensc-tool; then

    if [ -z "$SSH_AUTH_SOCK" ]; then

    if opensc-tool -l | grep -q "$_card_name"; then
        if ssh-add -l | grep -q opensc-pkcs11; then

        ssh-add -s "$_lib_dir/opensc-pkcs11.so"


    # If card is no longer present, remove the key.
    if ssh-add -l | grep -q opensc-pkcs11; then
        ssh-add -e "$_lib_dir/opensc-pkcs11.so" > /dev/null

Yubikey PIV Authentication

This is all well and good but I wanted to have stronger authentication for scenarios when I'm not on one of my computers. I also wanted to ensure other users of my systems were protected since I can't force them to use PIV cards for authentication. I did a little research and found pam_oath which supports both sequence based and time based one time passwords. This means it is compatible with the OTP profile on the Yubikey and authenticator based apps like Google Authenticator.

The parts I did with Puppet

The first part is pretty straightforward so I setup a Puppet module to do it for me. You need to have the PAM module installed, add it to the sshd pam.d policy, and update your sshd_config.

I am using Debian 9 but you should be able to adapt the following to most Puppet setups and distributions.

# Setup OATH (HTOP) modules/oath/manifests/init.pp
class oath {
  package { 'libpam-oath':
    ensure => latest,

  package { 'oathtool':
    ensure => latest,

  service { 'sshd':

  file { '/etc/users.oath':
    ensure => present,
    owner => root,
    group => root,
    mode => '0600',

  augeas { 'add pam_oath.so':
    context => "/files/etc/pam.d/sshd",
    changes => [
      'ins 01 after include[. = "common-auth"]',
      'set 01/type auth',
      'set 01/control required',
      'set 01/module pam_oath.so',
      'set 01/argument[last()+1] usersfile=/etc/users.oath',
      'set 01/argument[last()+1] window=20',
    onlyif => 'match /files/etc/pam.d/sshd/*/module[. = "pam_oath.so"] size == 0',

  augeas { 'set ChallengeResponseAuthentication':
    context => '/files/etc/ssh/sshd_config',
    changes => [
      'set ChallengeResponseAuthentication yes',
    onlyif => 'match /files/etc/ssh/sshd_config/ChallengeResponseAuthentication != "yes"',
    notify => Service['sshd'],

Things I didn't do with Puppet

The last thing you need is to initalize your shared secrets. I didn't want to do this with Puppet since I felt the need to control where the secrets were and minimize their exposure. The way I have pam_oath configured they will ultimately live in a file called /etc/users.oath. Make sure this is owned by root and has mode 0600. There are two examples that follow for creating the secret. One is for the OATH-HOTP used in the Yubikey, the other is for TOTP which most authenticator apps use (I use the One Time Password feature built into the Hurricane Electric Network Tools app on iOS, but I tested this with Google Authenticator as well).

Create a secret for the Yubikey

> dd if=/dev/urandom count=1 bs=1k 2>/dev/null | sha256sum

Put the returned hexadecimal string into your Yubikey as the shared secret.

Yubico Personalization Tool

Create a secret for an authenticator app

> dd if=/dev/urandom count=1 bs=1k 2>/dev/null | sha256sum | cut -b 1-30
> oathtool --totp -v <hexadecimal key from above>

So if your randomly generated key is 01ab5d053493a266172b16248a8377 then you would see:

imladris@15:27:29 ~ >oathtool --totp -v 01ab5d053493a266172b16248a8377
Hex secret: 01ab5d053493a266172b16248a8377
Digits: 6
Window size: 0
Step size (seconds): 30
Start time: 1970-01-01 00:00:00 UTC (0)
Current time: 2018-09-15 19:37:58 UTC (1537040278)
Counter: 0x30DC773 (51234675)


I used the Python module qrcode which includes a command line utility called qr to generate the configration QR code for the authenticator app. Using the above output of oathtool as an example this is how I made the QR code.

qr "otpauth://totp/<user>@<host>?secret=AGVV2BJUSORGMFZLCYSIVA3X" > qr.png

You can find more information about the otpauth:// uri format on Google's GitHub wiki.


Regardless of which mode you are using you'll need to add the secret to the users.oath file (I am using the example secret from above).

# Yubikey
HOTP    user1   -   01ab5d053493a266172b16248a8377
# Authenticator App
HOTP/T30/6  user2   -   01ab5d053493a266172b16248a8377    

The second line automatically increments the code every 30 seconds. In both cases they will expect a 6 digit code but in the second form it is explicit.

SSH with One Time Password

You can have different users with different methods. There are more complex PAM methods available if you don't want everyone to be required to use MFA or key based authentication, for example this is a good writeup that includes group or host exclusions. I feel like that provides an attacker the ability to work around your MFA.

In the end this probably took me long to write about than actually do and it's enhanced the security of my systems without any negative impact with one exception.

I use Panic's Transmit from time to time and I'm cheap so I have not upgraded to 5.0 yet. It turns out that they don't support the OTP prompt (in 4.0 at least). You can use a custom SSH key, and I believe you can restrict that key to sftp only so I may look into that as a workaround.

In any case there is really no reason you can't secure your servers now.


Comment via e-mail. Subscribe via RSS.