Two-factor authentication (2FA) requires not just knowledge of a password/key, but also possession of a physical device (a smartphone) with a one-time TOTP code. Even if an SSH key leaks, an attacker won't be able to log in without the second factor.
2FA for SSH via Google Authenticator
apt install libpam-google-authenticator -y
# Configure for each user (run as the user!)
su - admin
google-authenticator
# Answer Y, Y, N, Y, Y
# Scan the QR code in Authy/Google Authenticator
Configuring PAM and sshd_config
# /etc/pam.d/sshd
auth required pam_google_authenticator.so
# /etc/ssh/sshd_config
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
# (requires a key AND a one-time code)
systemctl reload sshd
Emergency Recovery Codes
# Location of the user's 2FA file
cat ~/.google_authenticator
# The first line is the secret key
# Lines with ** are recovery codes (one-time)
2FA for WordPress
Install the WP 2FA or Two Factor Authentication plugin:
- Install the plugin from the WordPress Repository
- Enable it for all administrators in the plugin settings
- Each user sets up their own TOTP at their next login
2FA for ISPmanager 6
# ISPmanager supports 2FA via TOTP
# Settings → Profile → Two-Factor Authentication
# Or via the API:
# POST /api/ispmgr/totp.set with the user's parameters
Supported Authenticator Apps
| App | Platform | Features |
|---|---|---|
| Authy | iOS, Android, Desktop | Sync across devices |
| Google Authenticator | iOS, Android | Simple, no sync |
| Microsoft Authenticator | iOS, Android | Cloud backup |
| Bitwarden (Premium) | All | Built into the password manager |
| 1Password | All | Built into the password manager |
Common 2FA Errors
| Symptom | Cause | Fix |
|---|---|---|
| Invalid verification code with a correctly entered code | The server's system clock is out of sync — TOTP is sensitive to drift beyond 1–2 minutes | Sync the clock: timedatectl set-ntp true and check timedatectl status |
| PAM authentication failure in auth.log with a correct code | The pam_google_authenticator.so line is in the wrong order relative to pam_unix.so or common-auth | Place it above @include common-auth in /etc/pam.d/sshd and restart sshd |
| The user lost their phone and never saved recovery codes | The ~/.google_authenticator file is tied to the device, with no backup codes left | Another admin logs in via SSH key and deletes/recreates the 2FA file for that user |
| "Time out of sync" in the authenticator app | The phone's clock has drifted from real time | Enable automatic time sync in the phone's settings |
Comparing Second-Factor Methods
| Method | Phishing resistance | Needs server-side support |
|---|---|---|
| TOTP (Google Authenticator) | None — the code can be intercepted and typed into a fake page | No, a PAM module is enough |
| SSH certificates (CA) | Yes, bound to the key pair, not to a typed code | Yes, requires its own certificate authority |
| U2F / WebAuthn (for web panels) | Yes, cryptographically bound to the domain — won't work on a phishing site | Yes, support must exist in the panel and the browser |
When 2FA Is Not a Good Fit
- Automated scripts and cron jobs that use an SSH key without an interactive session — TOTP via AuthenticationMethods will block that login
- A large distributed team with frequent membership changes — centralized management is easier with SSH certificates than everyone's personal TOTP secret
- An emergency scenario with no second admin having access — if the one person with 2FA loses their phone and codes, recovery needs console access through the hosting provider
- A low-value test VPS used for personal experiments — the extra login step isn't worth it for the risk involved
The baseline sshd_config setup this scheme relies on is covered in "SSH Hardening: Secure Server Configuration Against Break-ins". For web panels like cPanel, see "Two-Factor Authentication in cPanel: setting up 2FA".
Verifying the Result
Confirm 2FA is actually enforced at login, not just configured:
# Confirm login with only the key, no TOTP, is rejected
ssh -o PreferredAuthentications=publickey user@server
# Expect a verification code prompt, not an instant login
# The secret file's permissions must be closed to other users
stat -c "%a %n" ~/.google_authenticator
# Check the active authentication methods in the current config
sshd -T | grep -i authenticationmethods
For a login method more resistant to key theft across a team, see "SSH Certificates: Login via a Certificate Authority".