A password alone is no longer enough to protect an SSH server exposed on the Internet. Brute-force attacks and credential leaks happen daily. Two-factor authentication (2FA) adds a decisive layer of security: even if your password is compromised, the attacker will not be able to connect without the temporary code generated by your physical device. This tutorial guides you step by step through setting up 2FA on SSH with Google Authenticator.
Prerequisites
Before you start, make sure you have the following:
- A Linux server (Debian, Ubuntu, CentOS or derivative) with root or sudo access
- The SSH service installed and working (OpenSSH Server)
- A smartphone with a TOTP app installed (Google Authenticator, Authy, FreeOTP, etc.)
- An alternative console access to the server (KVM, IPMI, cloud console) in case of lockout
- The NTP service enabled to ensure clock synchronization
Always keep an SSH session open while configuring 2FA. Never close your active session before verifying that the new configuration works in a second connection. A configuration error can permanently lock you out of your server.
How the TOTP protocol works
TOTP (Time-based One-Time Password) is an algorithm defined by RFC 6238 that generates time-based one-time passwords. The principle relies on a secret shared between the server and your mobile device.
It works as follows:
- During setup, a shared secret (a 160-bit key encoded in Base32) is generated on the server side and transmitted to your mobile app via a QR code
- At every 30-second interval, the app and the server independently compute a 6-digit code by applying the HMAC-SHA1 algorithm to the secret combined with the current timestamp
- At login, the server compares the code you enter with the one it computed. If the two match, authentication is validated
The major advantage of TOTP is that it requires no network connectivity on the mobile device: the computation is done entirely offline, only clock synchronization is required.
Installing libpam-google-authenticator
The Google Authenticator PAM module is available in the official repositories of most Linux distributions.
On Debian / Ubuntu:
sudo apt update
sudo apt install libpam-google-authenticator -y
On CentOS / RHEL / Rocky Linux:
sudo dnf install epel-release -y
sudo dnf install google-authenticator -y
On Arch Linux:
sudo pacman -S libpam-google-authenticator
Verify that the PAM module is correctly installed:
ls /lib/security/pam_google_authenticator.so
# or on some distributions:
ls /lib64/security/pam_google_authenticator.so
Configuring Google Authenticator
Run the configuration command as the user who will connect over SSH (not as root, unless you connect as root):
google-authenticator
The interactive wizard will ask you several questions:
Do you want authentication tokens to be time-based (y/n) y
# An ASCII QR code is displayed here
# Scan it with your TOTP app
Your new secret key is: JBSWY3DPEHPK3PXP
Enter code from app (-1 to skip): 123456
Your emergency scratch codes are:
12345678
87654321
11223344
44332211
99887766
Do you want me to update your "/home/user/.google_authenticator" file? (y/n) y
Do you want to disallow multiple uses of the same authentication token? (y/n) y
By default, a new token is generated every 30 seconds. Do you want to increase the window size? (y/n) n
Do you want to enable rate-limiting? (y/n) y
Immediately write down the emergency scratch codes displayed. Store them in a safe, offline location (physical safe, encrypted password manager). These codes will let you connect if you lose access to your TOTP app.
The configuration is stored in the ~/.google_authenticator file. This file contains the secret, the options and the remaining backup codes. Protect it:
chmod 600 ~/.google_authenticator
ls -la ~/.google_authenticator
Configuring PAM
PAM (Pluggable Authentication Modules) is the system that handles authentication on Linux. We need to add the Google Authenticator module to it for SSH.
Edit the PAM configuration file for SSH:
sudo nano /etc/pam.d/sshd
Add the following line at the end of the file:
auth required pam_google_authenticator.so nullok
The available options for the PAM module are:
nullok: allows users who have not yet configured 2FA to connect normally. Remove this option once all users have configured their 2FAno_increment_hotp: do not increment the counter on failure (useful in HOTP mode only)noskewadj: disables automatic time skew adjustmentecho_verification_code: displays the code entered (not recommended in production)secret=/path/to/${USER}/.google_authenticator: custom path to the configuration file
The
nullok option is convenient during a gradual 2FA rollout, but it is a security hole if left in place. A user who has not configured 2FA will be able to connect with just their password. Plan to remove it as soon as all accounts are configured.
Configuring the SSH server
Modify the SSH daemon configuration to enable challenge-response authentication:
sudo nano /etc/ssh/sshd_config
Modify or add the following directives:
# Enable challenge-response authentication
KbdInteractiveAuthentication yes
# Define the required authentication methods
# Password + TOTP code
AuthenticationMethods keyboard-interactive
# Make sure UsePAM is enabled
UsePAM yes
On recent OpenSSH versions (8.7+), the
ChallengeResponseAuthentication directive has been renamed to KbdInteractiveAuthentication. Both are accepted for backward compatibility, but prefer the new syntax.
Check the configuration syntax before restarting:
# Test the configuration
sudo sshd -t
# If there are no errors, restart the service
sudo systemctl restart sshd
Test immediately in a new terminal (keep your current session open):
ssh user@your-server
You should see a password prompt followed by a verification code prompt:
Password:
Verification code:
2FA combined with SSH keys
The most secure configuration combines SSH key authentication and the TOTP code. The user must then present two factors: something they own (the private key) and something they know/generate (the TOTP code).
Modify /etc/ssh/sshd_config:
# SSH key + TOTP code authentication
AuthenticationMethods publickey,keyboard-interactive
# Enable both methods
PubkeyAuthentication yes
KbdInteractiveAuthentication yes
UsePAM yes
Then, modify /etc/pam.d/sshd to comment out or remove the standard password authentication line, so that PAM only asks for the TOTP code:
# Comment out this line to avoid asking for the password via PAM
# @include common-auth
# The Google Authenticator module only asks for the TOTP code
auth required pam_google_authenticator.so
The login flow will then be:
- The client presents its public SSH key (automatic verification)
- If the key is accepted, the server asks for the TOTP code via keyboard-interactive
- The user enters their 6-digit code
- The connection is established if both factors are validated
# Restart SSH after modification
sudo sshd -t && sudo systemctl restart sshd
Exemptions for certain users
Some accounts (automated services, deployment scripts, monitoring) cannot provide a TOTP code. Use the Match directives to create exceptions.
Per-user exemption:
# Global configuration: key + TOTP for everyone
AuthenticationMethods publickey,keyboard-interactive
# Exemption for a specific user
Match User deploy,monitoring
AuthenticationMethods publickey
Per-group exemption:
# Create a group for the exempted accounts
sudo groupadd ssh-notp
# Add the users to the group
sudo usermod -aG ssh-notp deploy
sudo usermod -aG ssh-notp monitoring
# In sshd_config
Match Group ssh-notp
AuthenticationMethods publickey
Per-IP-address exemption (internal network for example):
# No 2FA from the local network
Match Address 192.168.1.0/24,10.0.0.0/8
AuthenticationMethods publickey
Each exemption is a potential attack surface. Limit exempted accounts to the strict minimum, use strong SSH keys (Ed25519) for these accounts, and regularly audit the list of exemptions. Prefer per-group exemptions to make management easier.
Recommended TOTP apps
The google-authenticator PAM module is compatible with any app that complies with RFC 6238. Here are the main options:
- Google Authenticator (Android/iOS): simple and effective, but no native cloud backup. If you change phones, you have to reconfigure each account
- Authy (Android/iOS/Desktop): encrypted cloud backup, multi-device synchronization. Ideal if you manage several servers
- FreeOTP (Android/iOS): open source, developed by Red Hat. No cloud backup, which is a security advantage
- Bitwarden Authenticator: integrated into the Bitwarden password manager, centralizes credentials and TOTP codes
- KeePassXC (Desktop): local password manager with built-in TOTP support, ideal for a 100% offline approach
To add an account manually (without a QR code), use the secret displayed during the google-authenticator command and configure:
- Type: TOTP (time-based)
- Algorithm: SHA1
- Digits: 6
- Period: 30 seconds
Managing backup codes
The backup codes (scratch codes) are your safety net. Each code can only be used once and is removed from the ~/.google_authenticator file after use.
Check the remaining codes:
# The backup codes are the 8-digit lines in the file
# The last 5 lines after the options
tail -5 ~/.google_authenticator
Regenerate the backup codes (full re-run of the configuration):
# Fully reconfigure Google Authenticator
google-authenticator
Re-running
google-authenticator generates a new secret. You will have to rescan the QR code in your TOTP app. The old secret and the old backup codes will be invalidated. Make sure to update your mobile app immediately.
Best practices for backup codes:
- Store them in an encrypted password manager (KeePassXC, Bitwarden)
- Print them out and keep them in a physical safe
- Never store them on the server itself
- Regenerate them as soon as only 2 are left
- Regularly test that a backup code works
Advanced hardening
Strengthen the TOTP configuration with additional options.
Rate limiting (limiting the number of attempts):
During the initial setup, answer y to the rate-limiting question. This limits attempts to 3 login attempts every 30 seconds. You can also edit the file directly:
# Check that rate limiting is active in ~/.google_authenticator
# The following line must be present:
# " RATE_LIMIT 3 30
grep RATE_LIMIT ~/.google_authenticator
Window size (time tolerance):
By default, the server accepts the current code plus the previous and next code (window size of 1, i.e. 3 valid codes). On a high-latency network, you can increase this window:
# In ~/.google_authenticator, modify or add:
# " WINDOW_SIZE 3
# Accepts the 3 previous and next codes (i.e. 7 valid codes)
# The larger the window, the weaker the security
Disallow token reuse:
# The following line in ~/.google_authenticator disallows reuse
# " DISALLOW_REUSE
grep DISALLOW_REUSE ~/.google_authenticator
Fail2ban for 2FA:
Configure Fail2ban to block repeated 2FA attempts:
# /etc/fail2ban/jail.local
# [sshd]
# enabled = true
# port = ssh
# filter = sshd
# logpath = /var/log/auth.log
# maxretry = 3
# bantime = 3600
# findtime = 600
sudo systemctl restart fail2ban
Troubleshooting
Problem: locked out of the server
If you can no longer connect over SSH:
- Access the server via the physical console, KVM, IPMI or your host's web console
- Temporarily disable 2FA:
# Option 1: comment out the PAM module
sudo nano /etc/pam.d/sshd
# Comment out the line: auth required pam_google_authenticator.so
# Option 2: revert to simple authentication
sudo nano /etc/ssh/sshd_config
# Change: AuthenticationMethods password
# Restart SSH
sudo systemctl restart sshd
Problem: the TOTP code is always rejected
Check the clock synchronization:
# Check the NTP status
timedatectl status
# If NTP is not synchronized
sudo timedatectl set-ntp true
# Force an immediate synchronization
sudo systemctl restart systemd-timesyncd
# Check the current time
date -u
Problem: debugging the PAM configuration
Enable debug mode to diagnose problems:
# In /etc/pam.d/sshd, add debug to the module
auth required pam_google_authenticator.so debug
# Follow the logs in real time during a login attempt
sudo tail -f /var/log/auth.log
# On systems with journald
sudo journalctl -u sshd -f
Problem: the server does not ask for the TOTP code
# Check that KbdInteractiveAuthentication is set to yes
sudo sshd -T | grep kbdinteractiveauthentication
# Check that UsePAM is enabled
sudo sshd -T | grep usepam
# Check that the .google_authenticator file exists for the user
ls -la ~/.google_authenticator
# Check the permissions (must be 600 or 400)
stat -c "%a %U" ~/.google_authenticator
Problem: "Permission denied" after configuration
# Check the home directory permissions
# SELinux or AppArmor may block access
ls -laZ ~/.google_authenticator
# On SELinux, restore the context
sudo restorecon -Rv ~/.google_authenticator
# Check that the home directory is not world-writable
ls -ld ~
Always use
sudo sshd -T (with an uppercase T) to display the effective SSH server configuration. This takes into account all included files and the Match directives. Compare the output with your expectations to identify discrepancies.
Conclusion
Two-factor authentication on SSH is an essential security measure for any server exposed on the Internet. The SSH key + TOTP combination offers the best level of protection by requiring both a possession factor (the private key and the phone) and a time-based factor (the TOTP code).
To recap the key points:
- Install
libpam-google-authenticatorand configure each user individually - Favor the
publickey,keyboard-interactivecombination for maximum security - Keep your backup codes offline and secure
- Use
nullokonly during the rollout phase - Keep NTP synchronization on all your servers
- Always test in a new session before closing the active session
- Complement 2FA with Fail2ban for defense in depth
The time invested in this configuration is minimal compared to the protection it brings. A 6-digit code entered in a few seconds can make the difference between a secure server and a complete compromise.
Comments