Two Factors Are Better Than One

Although I've always been interested in security, there are just some security measures I've never liked. SSH brute-force attacks end up being a major way that attackers compromise Linux systems, but when it comes to securing SSH, I've never been a fan of changing your SSH port to something obscure, nor have I liked scripts like fail2ban that attempt to detect brute-force attacks and block attackers with firewall rules. To me, those measures sidestep the real issue: brute-force attacks require password authentication. If you disable password authentication (set PasswordAuthentication to no in your sshd_config) and use only SSH keys, you can relax about all those brute-force attacks knocking on your door.

In a past article ("Secret Agent Man", December 2013), I wrote about why you should set a passphrase on your SSH keys and how to use SSH Agent to make password-protected keys a bit less annoying. In one respect, you can think of password-protected SSH keys as a form of two-factor authentication. The key is something you have, and the password is something you know. The problem, however, is that if you host a system with multiple users, you can't enforce password-protected SSH keys from the server side. So in this article, I discuss how to add two-factor authentication to an SSH server that accepts only keys.

These days, more services on-line offer two-factor authentication (2FA) as an extra layer of security on top of a user name and password. After you perform your normal authentication, you provide your 2FA token (usually a string of digits) that authenticates you. Although in the past, 2FA required you to carry around a special hardware dongle, these days, a number of software approaches can use your cell phone instead. Some approaches use TOTP (Time-based One-Time Password), so your phone just needs accurate time but no network to function. Other approaches use push notifications, SMS or even a phone call to share the 2FA token, and some implementations can use all of the above.

Some 2FA SSH implementations work via the ForceCommand directive placed in the SSH configuration for a particular user and let you enable 2FA on a per-user basis. Others offer a PAM module you can add system-wide (and use for sudo authentication as well as SSH). Although a number of excellent 2FA SSH implementations exist for Linux, I've chosen Google Authenticator for a few reasons:

  • It's free, and the source is available.

  • It's been available and tested for a number of years.

  • Packages are available for a number of distributions.

  • Clients are available for a number of phone operating systems.

  • It uses a custom PAM module, so it's easy to add 2FA system-wide.

  • It provides a backup in the form of backup codes in case users lose or wipe their phones.

Install Google Authenticator

As I mentioned, Google Authenticator is packaged for a number of distributions, so, for instance, on Debian-based systems, you can install it with:

$ sudo apt-get install libpam-google-authenticator

If for some reason it isn't packaged for your distribution, you also can just go here, download the software and make and install it according to the documentation there. You also will need to install the Google Authenticator app on your phone.

Configure User Accounts

I recommend setting up Google Authenticator for all of your user accounts (or at least all of the sysadmin accounts) before enforcing 2FA in SSH to make it easier to enroll all of the users and avoid the risk of locking people out. To configure Google Authenticator, each user needs to log in and run google-authenticator. You will be presented with a series of questions where it's safe to answer "y"; however, I generally answer no to extending the time window to four minutes, and I also answer no to rate limiting, since as I disable password authentication, I'm less concerned with brute-force attacks. The output looks something like this:

$ google-authenticator

Do you want authentication tokens to be time-based (y/n) y|0&cht=qr&chl
QR Code Removed
Your new secret key is: 4SK2LTLCTLCEV757
Your verification code is 221544
Your emergency scratch codes are:

Do you want me to update your "/home/username/.google_authenticator"
 ↪file (y/n) y

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it
increases your chances to notice or even prevent man-in-the-middle
attacks (y/n) y

By default, tokens are good for 30 seconds and in order to
compensate for possible time-skew between the client and the
server, we allow an extra token before and after the current time.
If you experience problems with poor time synchronization, you can
increase the window from its default size of 1:30min to about 4min.
Do you want to do so (y/n) n

If the computer that you are logging into isn't hardened against
brute-force login attempts, you can enable rate-limiting for the
authentication module. By default, this limits attackers to no
more than 3 login attempts every 30s. Do you want to enable
rate-limiting (y/n) n

If you have libqrencode installed, the output also will contain a QR code in the console you can scan with the Google Authenticator app on your phone. Otherwise, you simply can enter the secret key into your Google Authenticator application on your phone. Also, be sure to write down those backup codes and store them in a safe place. These are one-time-use codes you can use to get back in to the system in case you ever lose or wipe your phone. Once you are logged back in, you can run google-authenticator again.

Configure PAM and SSH

Once your phone and user accounts are configured with Google Authenticator, you are ready to enforce 2FA in PAM and SSH. To do this, edit your /etc/pam.d/sshd file and add the following to the top of the file:

auth required

On my Debian system, I noticed that once I finished the configuration process, I would not only be prompted for my 2FA token, I'd also be prompted for my local system password. Because I wasn't interested in three-factor authentication (two-and-a-half factor authentication?), I noticed I needed to comment out the following further down in the file:

@include common-auth

Of course, if you aren't on a Debian-based system, this extra step may not be necessary.

The final step is to configure SSH. Hopefully you already have disabled password authentication for SSH in the past, and if not, I recommend you consider it. Most of the SSH 2FA guides out there (this one included) will tell you to enable ChallengeResponseAuthentication in your /etc/ssh/sshd_config:

ChallengeResponseAuthentication yes

I noticed, however, that when you are using key-based authentication instead of passwords, you need to add an additional setting to the config file:

AuthenticationMethods publickey,keyboard-interactive

Once these settings are in place, you can enable them by restarting your SSH service, which depending on your system may be one of the following:

$ sudo service ssh restart
$ sudo service sshd restart

After SSH has restarted, you should get an additional prompt the next time you SSH to the server:

$ ssh
Authenticated with partial success.
Verification code:

Type in the verification code that shows up in your Google Authenticator phone app, and you can log in. The nice thing about adding 2FA to SSH is that it provides an additional means of protection in case your computer is ever compromised or stolen. Attackers also would have to compromise or steal your phone before they could access your systems.

Kyle Rankin is a Tech Editor and columnist at Linux Journal and the Chief Security Officer at Purism. He is the author of Linux Hardening in Hostile Networks, DevOps Troubleshooting, The Official Ubuntu Server Book, Knoppix Hacks, Knoppix Pocket Reference, Linux Multimedia Hacks and Ubuntu Hacks, and also a contributor to a number of other O'Reilly books. Rankin speaks frequently on security and open-source software including at BsidesLV, O'Reilly Security Conference, OSCON, SCALE, CactusCon, Linux World Expo and Penguicon. You can follow him at @kylerankin.

Load Disqus comments