SSH can be secured with two-factor authentication, but it’s not just about adding a second code; it’s about layering cryptographic trust on top of something you possess.

Let’s see how it works in practice. Imagine a user, alice, trying to SSH into a server.

# On alice's local machine
ssh alice@your_server_ip

# Server prompts for password
Password:
# Server then prompts for OTP
Verification code:

This flow is powered by sshd (the SSH daemon) and pam (Pluggable Authentication Modules), a flexible authentication framework. When sshd is configured to use PAM for authentication, it delegates the actual verification process to the PAM stack. We’ll be modifying sshd’s PAM configuration to include an OTP (One-Time Password) module.

The core problem this solves is that passwords, even strong ones, can be compromised through phishing, brute-force attacks, or credential stuffing. Two-factor authentication mitigates this by requiring a second factor, typically something you have (like a phone with an authenticator app) or something you are (like a fingerprint), in addition to something you know (your password).

Here’s the breakdown of what’s happening under the hood. sshd is configured to use PAM. The PAM configuration for sshd (usually found at /etc/pam.d/sshd) dictates the sequence of authentication checks. We’ll add a module that interacts with your chosen OTP system. For this example, we’ll use Google Authenticator via the libpam-google-authenticator package.

First, ensure you have the necessary package installed on the server:

sudo apt update
sudo apt install libpam-google-authenticator

Next, each user who needs 2FA must run the google-authenticator command on the server to generate their secret key and QR code.

# On the server, as the user 'alice'
google-authenticator

This command will output a QR code (which you can scan with your phone’s authenticator app like Google Authenticator, Authy, or FreeOTP), a secret key, and several emergency scratch codes. It will ask you a series of questions. For most setups, answering yes to all of them is appropriate:

  • Do you want authentication tokens to be time-based (y/n): y (This is for TOTP, the most common type)
  • Do you want me to update your "/home/alice/.google_authenticator" file? (y/n): y
  • By default, tokens are good for 30 seconds... Do you want to enable:: y (This is for time drift)
  • Do you want to disallow multiple uses of the same authentication token?: y (Crucial for security)
  • By the time you are reading this message, the token will have already expired... Do you want to enable:: y (This allows codes generated within the last 30 seconds before your clock syncs up)
  • Do you want to enable rate-limiting?: y (This prevents brute-force attacks on the OTP codes)

After running this, alice’s authenticator app will show a 6-digit code that refreshes every 30 seconds.

Now, we need to tell sshd to use this. Edit the PAM configuration file for sshd:

sudo nano /etc/pam.d/sshd

Add the following line at the very top of the file:

auth required pam_google_authenticator.so

This line tells PAM that for authentication (auth), it is required to successfully pass the pam_google_authenticator.so module. If this module fails, authentication fails.

Finally, you need to configure sshd itself to allow PAM authentication for 2FA. Edit the SSH daemon configuration:

sudo nano /etc/ssh/sshd_config

Ensure these lines are present and uncommented (or add them if they don’t exist):

ChallengeResponseAuthentication yes
UsePAM yes

ChallengeResponseAuthentication yes allows sshd to prompt the user for additional information beyond just a password, which is what the OTP module does. UsePAM yes explicitly enables PAM for sshd.

After making these changes, restart the SSH service:

sudo systemctl restart sshd

Now, when alice tries to SSH, after entering her password, she will be prompted for her verification code.

The most surprising true thing about this setup is that sshd itself doesn’t know anything about two-factor authentication. It simply delegates the entire authentication process to PAM, and PAM, in turn, is configured to delegate parts of that process to specific modules like pam_google_authenticator.so. This modularity is why you can plug in different authentication methods without altering sshd’s core logic.

The one thing most people don’t know is how critical the order of lines in /etc/pam.d/sshd is. If you put auth required pam_google_authenticator.so after other authentication modules (like pam_unix.so for password checking), you might be able to authenticate with just a password, bypassing the OTP entirely, depending on the required vs. requisite flags and the overall PAM stack logic. Placing it at the top ensures it’s the first hurdle.

The next concept to explore is how to use SSH keys in conjunction with two-factor authentication for even stronger security.

Want structured learning?

Take the full Cdk course →