Skip to content
shellhacks.dev
SSH and Networking

How to Use Two Factor Authentication With SSH Step by Step

Add two factor authentication to SSH with a TOTP app: install the PAM module, enrol users, require a key plus a code, and keep a way back in.

6 min read
Summarize in AI
Illustration for "How to Use Two Factor Authentication With SSH Step by Step": a terminal session, a padlock and key, gears.

A stolen laptop with an unprotected key is a stolen server. A second factor makes the key alone insufficient, and the cheapest one is a time-based one-time code from a phone app. The setup touches PAM and sshd, two places where mistakes lock you out, so the order and the safety steps matter. This follows hardening sshd. The commands assume Debian 13 with OpenSSH 10.0 and libpam-google-authenticator 1.11.

Enrol the user

Shell
sudo apt install libpam-google-authenticator
google-authenticator -t -d -f -r 3 -R 30 -w 3

Run it as the user who will log in. The flags choose time-based codes, disallow reuse of a code, write the file without asking, limit to 3 attempts per 30 seconds and allow a small window for clock drift. It prints a QR code to scan with an authenticator app, a secret key, and five emergency scratch codes. Store the scratch codes somewhere safe and offline. The secret lives in ~/.google_authenticator, mode 0400.

Configure PAM

/etc/pam.d/sshdText
# @include common-auth
auth required pam_google_authenticator.so nullok

Commenting out @include common-auth removes the system password prompt from the SSH login; otherwise users are asked for their account password as well as the code. nullok lets accounts without a secret file log in with a key only, which is useful during rollout and should be removed once everyone is enrolled. Keep a backup of the original file before editing.

Configure sshd

/etc/ssh/sshd_config.d/00-mfa.confText
UsePAM yes
KbdInteractiveAuthentication yes
PasswordAuthentication no
AuthenticationMethods publickey,keyboard-interactive
Shell
sudo sshd -t && sudo systemctl reload ssh
ssh [email protected]
Text
Authenticated with partial success.
Verification code:

The comma in AuthenticationMethods means both must succeed in order: first the key, then the code. A space between methods would mean either path is enough, which would defeat the purpose. Test from a second terminal while the first stays open. If the prompt never appears, check that UsePAM yes is set and that /var/log/auth.log or the journal shows the PAM module loading.

Exceptions for automation

Text
Match User deploy-ci Address 203.0.113.0/24
    AuthenticationMethods publickey

Unattended accounts cannot type a code. Give them a separate key, restrict it with from= and command= options in authorized_keys, and restrict the account by address as above, so the exception is narrow.

Troubleshooting

Codes depend on the clock: a server or phone that is a minute off rejects valid codes, so check timedatectl. Home directories on encrypted or network storage that are not mounted at login hide the secret file; set secret=/var/lib/google-auth/${USER} as a module option to keep it elsewhere. The module's logs go to the auth log. Restoring access without a code requires console access to remove the PAM line.

Where this does not apply

TOTP codes can be phished in real time, unlike hardware keys, which are bound to the site; see hardware keys with SSH for the stronger option. A shared bastion host is the place to put this once, rather than on every server, with the others accepting only connections from it, as described in jump hosts.

What the second factor protects and what it does not

A time-based one-time password is a six-digit code derived from a shared secret and the current time, and it changes every thirty seconds. Adding it to SSH means that a stolen password or private key alone no longer opens the door: the attacker also needs the phone or app that holds the secret. That protects against leaked credentials and against a key copied from an unencrypted backup. It does not protect against a compromised workstation where an attacker can wait for you to log in and ride the session, against an attacker who tricks you into entering a code on a fake page, or against loss of the phone without a recovery plan. A hardware key with FIDO2 is stronger against phishing, and TOTP is available everywhere and needs no hardware, which is why it is a common first step. State clearly in your documentation which attacks the setup covers, so nobody assumes more.

Clock accuracy matters. The code depends on the time on both the server and the phone, and a server that has drifted by more than a minute rejects valid codes. Keep the server synchronized with chrony or systemd-timesyncd.

Rolling it out without locking people out

Enrol one administrator first and keep a second session open while testing. In the PAM configuration, the nullok option on the Google Authenticator module lets users who have not yet enrolled log in without a code, which allows a gradual rollout, and you remove it once everyone is enrolled. Tell users to save the emergency scratch codes the setup prints, in a password manager or on paper in a safe place, since they are the only way back in if the phone is lost. Decide the policy for service accounts and automation: a script cannot type a code, so those accounts use key-only authentication in a Match block with AuthenticationMethods publickey, and the key is restricted by source address and, where possible, by a forced command. Without that, the first nightly job fails at midnight and somebody weakens the whole setup to fix it.

How the pieces fit in the configuration

Three settings must agree. PAM must ask for the code: the SSH PAM file gets a line for the authenticator module, and the line that includes the common password prompt is commented out if you want keys plus codes with no password. The daemon needs UsePAM yes and KbdInteractiveAuthentication yes, and AuthenticationMethods publickey,keyboard-interactive states that both steps are required in that order. If any one is missing, the symptoms mislead: the daemon accepts the key and never asks for a code, or asks for a code and a password, or rejects everything. Read the daemon log and sshd -T output for the effective methods, and test with ssh -v to see which methods the server offers after the key succeeds. Keep the earlier advice about a second session, and write down how to disable the second factor in an emergency from the console.

Recovery and ongoing care

Plan for the day someone loses a phone. The administrator needs a documented way to reset a user's secret: remove the .google_authenticator file in the user's home directory and have them enrol again, after confirming their identity by another channel. Keep a console or out-of-band route that does not depend on SSH, and test it. Review the list of enrolled users when people leave, and remove their keys and secrets together. Watch the logs for repeated failures at the code step, which can mean a user with a wrong clock or someone trying a stolen key. Rate limiting on the daemon, through MaxAuthTries and the firewall, limits guessing of the six-digit code, which has only a million possibilities, so do not rely on the code alone against an attacker who can try many times.

See AuthenticationMethods in sshd_config(5) and the PAM module's README.

Also inSecurity
Taggedssh
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.