How to Add Two-Factor Authentication to SSH (Keys Plus an Authenticator Code)

2FA for SSH: key + phone code — A stolen laptop is a stolen key. Not any more.

SSH keys (tutorial 3) already beat passwords by a mile. But a key is a file, and a stolen laptop is a stolen key. Adding a time-based one-time code — the same six digits an authenticator app shows for your bank — means an attacker needs the key and your phone. Twenty minutes of setup, and an extra five seconds per login that you'll stop noticing within a week.

Quick reference

StepWhat
Installsudo apt install libpam-google-authenticator -y
Enrol (as your user)google-authenticator → scan QR with any TOTP app
PAM/etc/pam.d/sshd → add auth required pam_google_authenticator.so
sshdKbdInteractiveAuthentication yes and AuthenticationMethods publickey,keyboard-interactive
Restartsudo systemctl restart ssh — keep a session open
Backupthe emergency scratch codes and ~/.google_authenticator

Before you start

Keep an SSH session open in a second window the whole time. If the new config is wrong, that window is how you fix it. Make sure the server's clock is synced (timedatectl) — TOTP codes depend on the time.

1. Install and enrol

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

Answer the questions: y to time-based tokens, scan the QR code with Google Authenticator, Authy, Aegis, 1Password or Bitwarden, then y to update the file, y to disallow reuse, n to the wider time window (unless your clocks drift), y to rate limiting.

Write down the emergency scratch codes. Each works once, with no phone.

Repeat for each user who logs in (the file is per-user: ~/.google_authenticator).

2. Tell PAM to ask for the code

sudo nano /etc/pam.d/sshd

At the top, add:

auth required pam_google_authenticator.so nullok

and comment out the line @include common-auth (put # in front) so the system password isn't also asked for. nullok lets users who haven't enrolled yet log in with just their key — remove it once everyone has.

3. Tell sshd to require both

sudo nano /etc/ssh/sshd_config

Set (or add) these lines:

KbdInteractiveAuthentication yes
PasswordAuthentication no
AuthenticationMethods publickey,keyboard-interactive

(Older Ubuntu calls the first one ChallengeResponseAuthentication.) AuthenticationMethods publickey,keyboard-interactive means: key first, then the interactive step, which PAM turns into the code prompt.

Check for typos before restarting:

sudo sshd -t
sudo systemctl restart ssh

4. Test from a new window

ssh john@192.168.1.50

You should get Verification code: after the key is accepted. Enter the six digits from the phone. In. Now, and only now, close the safety window.

Skipping the code on your home network

If you only want the code when connecting from outside, before the auth required line in /etc/pam.d/sshd add:

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

and create /etc/security/access-local.conf:

+ : ALL : 192.168.1.0/24
- : ALL : ALL

Connections from the LAN skip the authenticator line; everything else still needs it.

Automated jobs

Scripts that SSH into the server (rsync backups from another machine, say) can't type a code. Give them a dedicated user with nullok still in place and no enrolment, or add a Match User backup block in sshd_config with AuthenticationMethods publickey.

Troubleshooting

No code prompt, logs straight in. nullok is set and the user hasn't enrolled, or AuthenticationMethods isn't set. journalctl -u ssh -f while logging in shows what PAM did.

"Permission denied" even with the right code. The clock. timedatectl on the server; the phone's clock too. Or ~/.google_authenticator has the wrong permissions — it must be 600 and owned by the user.

Locked out. Use the safety session, or the console (tutorial 37): remove the PAM line and restart ssh.

Lost the phone. A scratch code gets you in; then google-authenticator again to enrol a new phone.

Wrapping up

Install, enrol, one line in PAM, three in sshd, test from a second window. Keep the scratch codes somewhere safe. Keys stop guessing; codes stop a stolen key. Together they're as close to unbreakable as a home server login gets.

Call us