SSH keys already make password guessing useless. Two-factor authentication (2FA) adds a second, independent factor: even a stolen private key isn’t enough without the six-digit code from your phone. It’s a strong extra layer for servers that several people access, or that hold sensitive data.

This guide uses the TOTP standard (time-based one-time passwords) via the libpam-google-authenticator module. Any authenticator app works: Google Authenticator, Microsoft Authenticator, Aegis, 1Password, Bitwarden and others.

Before you start

  • Keep an open root session while configuring, and test logins in a new terminal.
  • Know how to use your VPS’s VNC console — your way back in if PAM is misconfigured.
  • Make sure the server clock is correct (timedatectl → synchronized: yes). TOTP codes depend on time.

1. Install the PAM module

sudo apt update
sudo apt install -y libpam-google-authenticator

(On AlmaLinux/Rocky, the package google-authenticator is in EPEL.)

2. Set up your user’s secret

Run this as the user who will log in (not with sudo):

google-authenticator

Answer:

  • Time-based tokens? Yes.
  • Scan the QR code with your authenticator app (enlarge the terminal if it’s cut off).
  • Save the emergency scratch codes somewhere safe — each works once if you lose your phone.
  • Update the .google_authenticator file? Yes.
  • Disallow multiple uses of the same token? Yes.
  • Increase the time window? No (unless your clocks drift).
  • Enable rate-limiting? Yes.

The secret is stored in ~/.google_authenticator (mode 400).

3. Configure PAM for sshd

Edit /etc/pam.d/sshd and add at the end:

auth required pam_google_authenticator.so nullok

nullok lets users who haven’t set up 2FA yet still log in — useful during rollout. Remove it once every user has configured 2FA.

If you use keys + TOTP (recommended, below), also comment out the line that asks for the Unix password, so users aren’t prompted for it:

# @include common-auth

4. Configure sshd

Create /etc/ssh/sshd_config.d/20-2fa.conf:

KbdInteractiveAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

AuthenticationMethods publickey,keyboard-interactive means: first a valid SSH key, then the TOTP code. Both are required.

(On older OpenSSH, the setting is ChallengeResponseAuthentication yes.)

Check for conflicting settings — files in sshd_config.d/ are read in alphabetical order and the first value wins:

sudo sshd -t
sudo sshd -T | grep -Ei 'kbdinteractive|authenticationmethods|usepam'
sudo systemctl reload ssh       # "sshd" on RHEL-family systems

5. Test in a new terminal

ssh [email protected]

You should be authenticated by your key, then prompted:

([email protected]) Verification code:

Enter the code from the app. Only close your original session when this works.

Exceptions: automation and trusted networks

Deploy scripts and backup jobs can’t type codes. Options:

Exempt specific users (e.g. a restricted deploy account) in sshd config:

Match User deploy
    AuthenticationMethods publickey

Exempt trusted source IPs (e.g. your VPN subnet) in PAM, before the google-authenticator line:

auth [success=1 default=ignore] pam_access.so accessfile=/etc/security/access-2fa.conf

with /etc/security/access-2fa.conf:

+ : ALL : 10.8.0.0/24
- : ALL : ALL

Keep exceptions narrow and documented.

Lost your phone?

  • Use one of the scratch codes to log in, then run google-authenticator again to create a new secret.
  • No codes left? Log in through the VNC console (it uses local login, which you haven’t changed) and remove or recreate ~/.google_authenticator.

Alternative: hardware security keys

OpenSSH 8.2+ supports FIDO2 keys natively (ssh-keygen -t ed25519-sk). The private key lives on the device and each login requires a touch — phishing-resistant and simpler than TOTP. See YubiKey for SSH.

Where 2FA fits

  1. SSH keys, passwords disabled.
  2. No root login.
  3. Firewall and Fail2Ban.
  4. 2FA for interactive admin logins.

See the Linux server hardening checklist for the full picture.

  • SSH
  • 2FA
  • Security