SSH two-factor authentication asks for a 6-digit code from an authenticator app in addition to your key or password. Even if a private key leaks, an attacker cannot log in without your phone. This guide uses the widely packaged Google Authenticator PAM module, which works with any TOTP app (Google Authenticator, Authy, Aegis, 1Password, Bitwarden).
Before you start
- Root access to an Ubuntu/Debian or AlmaLinux/Rocky VPS.
- SSH key login already working for your user.
- An authenticator app on your phone.
- Console/VNC access from your provider and a second SSH session kept open while you test.
- Accurate server time: TOTP depends on it. Check
timedatectlshows System clock synchronized: yes.
Install the PAM module
Ubuntu/Debian:
apt install libpam-google-authenticatorAlmaLinux/Rocky (EPEL):
dnf install epel-release && dnf install google-authenticator qrencodeEnrol your user
Run as the user who will log in (not with sudo):
google-authenticator -t -d -f -r 3 -R 30 -w 3This creates time-based tokens, disallows code reuse, limits attempts to 3 per 30 seconds and allows a small clock drift. Scan the QR code in your app and save the emergency scratch codes in your password manager. Each works once if you lose your phone. The secret is stored in ~/.google_authenticator.
Configure PAM for SSH
Edit /etc/pam.d/sshd and add at the end:
auth required pam_google_authenticator.so nulloknullok lets users who have not enrolled yet log in without a code; remove it once every user is enrolled. If you use keys only (recommended), also comment out the password prompt so users are not asked for a password after their key:
#@include common-authOn AlmaLinux/Rocky the equivalent line is auth substack password-auth.
Configure sshd
In /etc/ssh/sshd_config (and check /etc/ssh/sshd_config.d/ for overrides):
KbdInteractiveAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactiveOn older OpenSSH versions the first option is called ChallengeResponseAuthentication yes. AuthenticationMethods requires a valid key and the code. Test and reload:
sshd -t && systemctl reload ssh || systemctl reload sshdWarning: keep your existing session open. Test a new login in a separate terminal before closing anything.
Test the login
ssh user@203.0.113.10After the key is accepted you should see Verification code:. Enter the current code from your app.
Exempt automation accounts
Deploy users and backup jobs cannot type codes. Exempt them with a Match block at the end of sshd_config:
Match User deploy
AuthenticationMethods publickeyKeep such accounts limited (key only, narrow sudo, restricted source IPs if possible).
Troubleshooting
- Code always rejected: server or phone time is off. Sync with
timedatectl set-ntp true. - Not asked for a code:
KbdInteractiveAuthenticationis off,AuthenticationMethodsis missing, or an override file insshd_config.dwins. - Asked for password and code: the common-auth / password-auth line is still active in
/etc/pam.d/sshd. - Lost phone: use a scratch code, then rerun
google-authenticatorto enrol a new device. If all else fails, log in through the provider console and remove the PAM line temporarily.
SSH 2FA FAQ
Is 2FA needed if I already use SSH keys?
Keys are strong on their own. 2FA adds protection if a key is stolen from a laptop or backup, which matters for servers holding customer data.
Can I use a hardware key instead?
Yes. OpenSSH 8.2+ supports FIDO2 keys directly with ssh-keygen -t ed25519-sk, which needs no PAM setup.
Does cPanel support 2FA?
Yes, cPanel and WHM have their own two-factor authentication for the web login, separate from SSH.
