2fa SSH on Linux, and deploying it via Puppet

Posted by Wxcafé on Fri 24 November 2017

So, I recently (on Monday, 11/20/17) started at a new job! Aside from all the new stuff that implies, and that is much more important overall, I received a Yubikey Neo key to be used for two-factor authentication on various internal services. I found this pretty cool, started using it, and thought no more of it.

Then the next day, I tried to SSH to my server for some reason and found out that I could obviously not log in since I didn’t have my key. So I thought for a minute, didn’t find any way to enter my infrastructure when I don’t have my key, and took note to do whatever I was planning that evening.

After a bit of time I put two and two together and thought I could probably try to add 2fa (TOTP, Time-based One Time Password + username) as an alternative authentication method to RSA public key. So I looked a bit and ended up setting that up. Here’s how it works.

So as you might know, authentication on Linux is handled by PAM (Pluggable Authentication Modules). As the name implies, it’s made of modules that you can simply plug onto the existing authentication mechanism. And thankfully, someone developped a TOTP module for PAM, which is named libpam_google_authenticator (which is not a very good name, but I’m not gonna complain if I can get it working). So the plan is to install that module (pretty easy, since it’s packaged by most linux distros), then configure ssh to use it to auth users.That’s done by editing the /etc/pam.d/sshd file, and setting it up like so:

# PAM configuration for the Secure Shell service

### This is the important part. It adds the module and marks it as a required
### authentication method.
##
auth required pam_google_authenticator.so
##

@include common-auth

# Disallow non-root logins when /etc/nologin exists.
account    required     pam_nologin.so

# Uncomment and edit /etc/security/access.conf if you need to set complex
# access limits that are hard to express in sshd_config.
# account  required     pam_access.so

# Standard Un*x authorization.
@include common-account

# SELinux needs to be the first session rule.  This ensures that any
# lingering context has been cleared.  Without this it is possible that a
# module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad]
pam_selinux.so close

# ... nothing else interesting until end of file

Now that this is done, we need to change sshd’s config, to allow it to present the prompt for the TOTP code and tell it to fallback to keyboard-interactive if publickey auth doesn’t work. To do that, we add these lines to/etc/ssh/sshd_config:

UsePam yes
ChallengeResponseAuthentication yes
AuthenticationMethods publickey keyboard-interactive:pam

Be careful, this will allow users to login with TOTP+pass even if PasswordAuthentication is set to no

Once this is done, we need to generate a secret for the TOTP generator. libpam_google_authenticator has a tool for this named …google-authenticator. Once you run it, it’ll ask a few questions, display a QR-code on the screen, give you a private key and a few recovery codes, and write all this into a file named ~/.google_authenticator. Scan the QR code or add the private key into your TOTP client (an app on your phone, a linux CLI client, whatever. I use OTP Auth on iOS.)

Once that’s done, you can try to use that authentication by running ssh -o PubkeyAuthentication=no whatever.space. It should ask you for your “Verification Code”, which is the TOTP, and then your password, and then let you log in. Done, you have 2fa on SSH now 😄

Now for the puppet setup:

    package {'libpam-google-authenticator':
        ensure => present,
    }

    file {'/etc/pam.d/sshd':
        ensure  => present,
        content => file('base/pam/sshd'),
        owner   => 'root',
        group   => 'root',
    }

    file {'/home/wxcafe/.google_authenticator':
        ensure  => present,
        content => file(modules/base/google_authenticator,
        owner   => 'wxcafe',
        group   => 'wxcafe',
        mode    => '0600',
    }

    file {'/etc/ssh/sshd_config':
        ensure  => present,
        content => file('base/ssh/sshd_config'),
    }

Yeah, I know, this is pretty generic. It was a bit harder for me because I use a git repo for my dotfiles, and as such I have a git block and an exec block to chmod 600 the .google_authenticator file, and I had a bit of trouble trying to use a file block inside another file block (setting the mode of the file inside dotfiles git repo) (if you’re wondering, puppet simply ignores the least-specific block here… I spent a while wondering why my files wouldn’t copy…)

Either way it should work alright. You might also notice that I just deploy the .google_authenticator file on every machine, and think that’s not a very good security practice. I think it’s alright, a TOTP is basically identical security-wise to 10 TOTPs, as long as the key doesn’t leak, and the increased usability of not having to keep tens of TOTP codes on my phone is clearly worth it.