Skip to content

Add a second factor to SSH

Severity: mediumApplies to: OpenSSH 8.2+Applies to: OpenSSH 10.x
The fix
Terminal window
# On your workstation, with a FIDO2 key plugged in:
ssh-keygen -t ed25519-sk -C "you@yubikey"
ssh-copy-id -i ~/.ssh/id_ed25519_sk.pub youruser@host

Key-only authentication rests on one assumption: that nobody else has your private key. That assumption fails quietly — a stolen laptop, a backup synced somewhere it shouldn’t be, a compromised workstation. Nothing on the server can tell the difference between you and a copy of your key.

A second factor breaks that. It requires something that can’t be copied over a network.

Hardware-backed keys (recommended). An ed25519-sk key’s private half is generated inside a FIDO2 authenticator and never leaves it. Authentication requires the device to be physically present and touched. There is no PAM configuration, no shared secret, no time sync, and no server-side state — from the daemon’s point of view it’s just a public key.

Requires OpenSSH 8.2+ on both ends. If your key should be useless without a touch even when plugged in, that’s the default; -O no-touch-required disables it and defeats the point.

TOTP via PAM. The traditional route: libpam-google-authenticator plus an AuthenticationMethods line requiring both a key and a code. It works and it needs no hardware, but it adds a PAM stack you can misconfigure, a per-user secret to enrol, and a dependency on clock skew.

/etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive:pam
KbdInteractiveAuthentication yes

Note this re-enables the keyboard-interactive path that key-only authentication turned off. That’s intentional here — but it means PAM is now reachable, so the PAM config is what stands between you and password logins. Get it wrong and you have reopened the door you closed.

AuthenticationMethods is where this control goes wrong, and the syntax gives no hint:

  • Comma means AND. publickey,keyboard-interactive requires both.
  • Space means OR. publickey keyboard-interactive accepts either — which is not two-factor authentication at all.

A stray space turns your MFA into an alternative password prompt while looking almost identical in the file. Verify the running config rather than reading it.