Two-Factor Authentication for SSH: TOTP Codes and FIDO2 Security Keys
SSH keys already beat passwords by a long way. But a private key is a file, and files get copied: by malware, from a stolen laptop, from a backup someone forgot to encrypt. Two-factor authentication makes a stolen key insufficient on its own.
There are two practical ways to do it on Linux:
- TOTP codes: the six-digit codes from an authenticator app, checked by the server through PAM
- FIDO2 security keys: SSH keys whose private half lives in a YubiKey or similar, requiring a physical touch
If SSH keys are new to you, start with SSH keys explained and the SSH hardening guide.
Before you change anything
Changing SSH authentication is the classic way to lock yourself out of a remote server. Every time:
- Keep an existing SSH session open while you test in a second terminal
- Validate the configuration before reloading:
sudo sshd -t
- Know how to reach the console (your VPS provider’s web console, Proxmox, IPMI) if it goes wrong
Method 1: TOTP with pam_google_authenticator
Despite the name, this module has nothing to do with Google services: it implements the standard TOTP algorithm, and works with any authenticator app, including Aegis, 2FAS, Ente Auth, or a password manager.
Install and enroll
sudo apt install libpam-google-authenticator # Debian / Ubuntu
sudo dnf install google-authenticator # Fedora (EPEL on RHEL)
sudo pacman -S libpam-google-authenticator # Arch
Then, as the user who will log in (not root):
google-authenticator
Answer yes to time-based tokens. It shows a QR code to scan with your app, and prints emergency scratch codes: save those somewhere safe, they are your way in if you lose the phone. Answer yes to updating the ~/.google_authenticator file, and to disallowing reuse of codes.
Configure PAM
Edit /etc/pam.d/sshd and add near the top:
auth required pam_google_authenticator.so
If you want to roll it out gradually, nullok lets users who have not enrolled yet log in without a code:
auth required pam_google_authenticator.so nullok
To require only key + code and not also prompt for a password, comment out the line that includes common password authentication on Debian and Ubuntu:
# @include common-auth
Our PAM explainer covers how these stacks work, and why order matters.
Configure sshd
Create /etc/ssh/sshd_config.d/50-2fa.conf:
UsePAM yes
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
The comma in AuthenticationMethods means both are required: a valid key first, then the verification code. (ChallengeResponseAuthentication in older guides is a deprecated alias for KbdInteractiveAuthentication.)
Test and reload, keeping your existing session open:
sudo sshd -t && sudo systemctl reload ssh # 'sshd' on Fedora/Arch
From a new terminal:
$ ssh alice@server
(alice@server) Verification code:
Method 2: FIDO2 hardware keys (ed25519-sk)
Since OpenSSH 8.2, SSH supports keys backed by FIDO2 security keys. The file in ~/.ssh is only a handle: the private key never leaves the hardware, and signing requires the key to be present and touched. Malware that copies your .ssh directory gets nothing usable.
Generate a key
Plug in the security key, then:
ssh-keygen -t ed25519-sk -C "laptop yubikey"
Touch the key when it blinks. Useful options:
| Option | Effect |
|---|---|
-O resident | Store the key handle on the security key, so you can use it from any machine with ssh-keygen -K |
-O verify-required | Also require the key’s PIN, not just a touch |
-O application=ssh:work | Separate resident keys by name |
-O verify-required turns it into true two-factor: something you have (the key) and something you know (its PIN).
If ed25519-sk fails, the security key may not support Ed25519; -t ecdsa-sk is the fallback. On Linux the key needs udev permissions, normally provided by the libfido2 package; see passkeys on Linux for setup.
Install the public key on the server
ssh-copy-id -i ~/.ssh/id_ed25519_sk.pub alice@server
The server needs OpenSSH 8.2 or newer, which every current distribution has. Nothing else changes server-side; it is a public key like any other.
Require it
To make hardware-backed keys the only accepted kind, list only sk- key types:
PubkeyAcceptedAlgorithms sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com
That rejects ordinary key files entirely. Make sure every account has a hardware key enrolled first.
Avoid locking yourself out
- Register two security keys per account (add both public keys to
authorized_keys), and keep one somewhere safe - Save TOTP scratch codes offline
- Consider leaving 2FA off for one break-glass admin account that only accepts logins from your VPN, using a
Match Addressblock - Keep provider console access working, and test it occasionally
TOTP or FIDO2?
| TOTP | FIDO2 key | |
|---|---|---|
| Hardware needed | Any phone | A security key (about the price of a nice lunch) |
| Phishing-resistant | No | Yes |
| Stops stolen key files | Yes (code also needed) | Yes (file is useless without hardware) |
| Daily friction | Type a code | Touch the key |
| Client support | Any SSH client | OpenSSH 8.2+ clients |
For your own machines, FIDO2 is both stronger and more pleasant. TOTP is easier to roll out to many users with mixed clients. They also combine with Fail2ban or CrowdSec for rate limiting, and with an SSH certificate authority for larger fleets.
Frequently Asked Questions
Is SSH key authentication not already two-factor?
Not quite. A key file is one factor, something you have, and a passphrase on it is a second factor only on your own machine, since the server cannot verify the passphrase was used. Server-enforced 2FA with TOTP, or a FIDO2 key that requires a physical touch, adds a factor the server can actually check.
What is an ed25519-sk key?
An SSH key whose private part lives on a FIDO2 hardware security key such as a YubiKey. The file on disk is only a handle; signing requires the physical key to be plugged in and touched. Supported since OpenSSH 8.2 on both client and server.
Which setting enables TOTP prompts in sshd?
KbdInteractiveAuthentication yes, together with UsePAM yes and the pam_google_authenticator module in /etc/pam.d/sshd. Older guides call the setting ChallengeResponseAuthentication, which is a deprecated alias for the same thing.
How do I require both a key and a code?
Set AuthenticationMethods publickey,keyboard-interactive in sshd_config. The comma means both are required in that order: the client must present a valid key, then answer the PAM prompt for the verification code.
What if I lose my phone or security key?
For TOTP, use one of the emergency scratch codes printed when you ran google-authenticator, which is why you should save them. For FIDO2 keys, register a second hardware key in authorized_keys, or keep console access available so you can log in and fix authorized_keys.
Should I use TOTP or a FIDO2 key?
A FIDO2 key is stronger and more convenient day to day, since it resists phishing and malware stealing key files and needs only a touch. TOTP works with any phone and any SSH client, which makes it easier to roll out to many users or older systems.