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-authenticatoragain 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
- SSH keys, passwords disabled.
- No root login.
- Firewall and Fail2Ban.
- 2FA for interactive admin logins.
See the Linux server hardening checklist for the full picture.