2026年11月14日
2026年11月14日
SSH Two-Factor Auth with Google Authenticator: Public Key + TOTP Code
This article is a localized edition of the Japanese original. Read the Japanese original
If you add the 6-digit code (TOTP) that a phone authenticator app (Google Authenticator, Microsoft Authenticator and so on) shows every 30 seconds to your SSH login, a leaked private key or password is no longer enough to log in.
On the server you use the PAM module pam_google_authenticator.so published by Google. The app is not something you install on the server. The flow is:
- Install the PAM module on the server
- For each user who logs in, create a secret with the
google-authenticatorcommand and register it in the app - Configure
/etc/pam.d/sshdandsshd_configso that SSH authentication also asks for the code
This article builds a setup where "public key authentication succeeds, and then you also enter a code." On Debian 13 (OpenSSH 10.0, libpam-google-authenticator 20191231) we confirmed that login works with the correct code and is refused with a wrong code or without the key. On AlmaLinux 9 we checked the packages and the layout of the configuration files (as of November 2026).
Before you start
- Do not close your current SSH connection until the final check is done. A mistake in the configuration can stop new logins. Also make sure you know how to use the console in your VPS control panel (the screen you can operate from a browser).
- Make sure you can log in with public key authentication. If you cannot yet, first follow How to set up SSH public key authentication.
- TOTP codes do not match if the server clock is off. Check that the time is synchronized.
timedatectl
# synchronized if it shows System clock synchronized: yes
1. Install the PAM module
# Debian / Ubuntu
sudo apt update
sudo apt install libpam-google-authenticator
# AlmaLinux / Rocky Linux / RHEL 9 (package from EPEL)
sudo dnf install epel-release
sudo dnf install google-authenticator
Watch the package names. On Debian/Ubuntu it is libpam-google-authenticator; on the RHEL family it is google-authenticator from EPEL. Both include the google-authenticator command that creates the secret and the PAM module.
2. Create a secret for each user
Run this as the user who will use two-factor authentication (without sudo). Running it as root creates the settings for root.
google-authenticator
It asks questions interactively. When you answer y to whether tokens should be time-based (TOTP), it prints a QR code and the secret key (a string of capital letters and digits) in the terminal.
- Scan the QR code with the authenticator app (if it cannot be scanned, type the secret key in by hand)
- At
Enter code from app (-1 to skip):, enter the code shown in the app - Note the emergency codes shown (8 digits, 5 of them by default) somewhere safe outside the server
- Answer the remaining questions (updating the settings file, disallowing code reuse, tolerated clock skew, rate limiting)
Instead of answering the questions, you can pass everything as options.
google-authenticator -t -d -f -r 3 -R 30 -w 3
| Option | Meaning |
|---|---|
-t |
Use time-based codes (TOTP) |
-d |
Disallow reuse of the same code |
-f |
Write ~/.google_authenticator without asking |
-r 3 -R 30 |
Limit attempts to 3 per 30 seconds |
-w 3 |
Accept the codes one step before and after as well (tolerates clock skew) |
Even with these options, the QR code is shown and the code from the app is confirmed (Enter code from app).
The settings are stored in ~/.google_authenticator. The first line is the secret, lines starting with " are settings, and the remaining 8-digit lines are emergency codes. Anyone who gets this file can generate codes, so do not change its permissions (-r--------, readable only by the owner).
3. PAM configuration (/etc/pam.d/sshd)
Before editing, keep copies of the original files.
sudo cp /etc/pam.d/sshd /etc/pam.d/sshd.bak
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
Add the following line to the end of /etc/pam.d/sshd.
auth required pam_google_authenticator.so
For a key-plus-code setup, comment out the line that asks for the password. If you leave it, you are asked for the Linux password in addition to the code.
# Debian / Ubuntu
#@include common-auth
# AlmaLinux / RHEL family
#auth substack password-auth
Users who have not set it up yet
With this configuration, users without ~/.google_authenticator can no longer log in (in the test environment, a user without the file was refused even with the correct key). During a transition period until everyone is set up, adding nullok, described in the official README, lets users without the file through without a code.
auth required pam_google_authenticator.so nullok
Remove nullok once everyone is set up.
4. sshd configuration
Create a new file in /etc/ssh/sshd_config.d/. When the same setting appears more than once, sshd uses the first value it reads, so give the file a name that is read before the existing files (a lower number).
sudo nano /etc/ssh/sshd_config.d/01-2fa.conf
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes: lets PAM ask for the code. The old nameChallengeResponseAuthenticationis a deprecated alias of this settingAuthenticationMethods publickey,keyboard-interactive: allow login only when public key authentication succeeds and the keyboard-interactive authentication (here, the code) also succeedsUsePAM yesis required. It is already enabled in the default configuration of Debian, Ubuntu and AlmaLinux
On AlmaLinux 9, /etc/ssh/sshd_config.d/50-redhat.conf contains ChallengeResponseAuthentication no. If you name your file 60-2fa.conf, it is read after that one and ignored; in the test environment the configuration check (sshd -T) printed the following error.
Disabled method "keyboard-interactive" in AuthenticationMethods list "publickey,keyboard-interactive"
AuthenticationMethods cannot be satisfied by enabled authentication methods
Check the configuration, then restart sshd.
sudo sshd -t
sudo sshd -T | grep -E "^(usepam|kbdinteractiveauthentication|authenticationmethods)"
usepam yes
kbdinteractiveauthentication yes
authenticationmethods publickey,keyboard-interactive
# Debian / Ubuntu
sudo systemctl restart ssh
# AlmaLinux / RHEL family
sudo systemctl restart sshd
5. Test the login from another terminal
Keep the current connection open, open a new terminal on your own computer and log in.
ssh user@server-address
It works if, after public key authentication, you see the prompt below and can log in by entering the code from the app.
(user@203.0.113.10) Verification code:
These were the results in the test environment.
| What we tried | Result |
|---|---|
| Correct key + correct code | Logged in |
| Correct key + wrong code | Verification code: was shown again and login failed |
No key (-o PubkeyAuthentication=no) |
Refused with Permission denied (publickey). |
User without ~/.google_authenticator (no nullok) |
Refused with Permission denied (keyboard-interactive). |
If it does not work, check the logs. pam_google_authenticator entries appear in /var/log/auth.log or journalctl -u ssh on Debian/Ubuntu, and in /var/log/secure or journalctl -u sshd on the RHEL family.
sshd(pam_google_authenticator)[1166]: Invalid verification code for user
Do not close the first connection until the check is complete. If you cannot log in, restore /etc/pam.d/sshd.bak and sshd_config.bak from the first connection, delete 01-2fa.conf and restart sshd.
If you lose your phone
- Emergency codes: enter an 8-digit emergency code at
Verification code:to log in. Each code works only once. - Set it up again: after logging in, the user runs
google-authenticatoragain. This creates a new secret and new emergency codes, and the old app registration stops working. - If nobody can log in: log in through the console in your VPS control panel and comment out the
pam_google_authenticator.soline in/etc/pam.d/sshd.
If you want the code on more than one phone, scan the QR code with each app when it is first displayed.
Things to watch
- Automated jobs that log in with a key only, such as SFTP or rsync, will also be asked for a code and fail. You need to handle them, for example with a separate user for automation and
AuthenticationMethods publickeyfor that user underMatch User. - If you back up the secret (
~/.google_authenticator), protect the backup as carefully as the key itself. - If you use public key authentication, turning off password logins as well improves security. The steps are in How to disable SSH password authentication.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)