Security
Difficulty: Intermediate
11 min read

SSH 2FA: Two-Factor Authentication with Google Authenticator

Set up two-factor authentication on SSH with Google Authenticator to strengthen the security of your Linux servers.

Back to tutorials
Why 2FA is essential
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
Lockout risk
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:

  1. 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
  2. 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
  3. 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
Backup codes
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 2FA
  • no_increment_hotp: do not increment the counter on failure (useful in HOTP mode only)
  • noskewadj: disables automatic time skew adjustment
  • echo_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
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
Note about ChallengeResponseAuthentication
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:

  1. The client presents its public SSH key (automatic verification)
  2. If the key is accepted, the server asks for the TOTP code via keyboard-interactive
  3. The user enters their 6-digit code
  4. 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
Security of exemptions
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
Warning
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:

  1. Access the server via the physical console, KVM, IPMI or your host's web console
  2. 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 ~
Debugging tip
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-authenticator and configure each user individually
  • Favor the publickey,keyboard-interactive combination for maximum security
  • Keep your backup codes offline and secure
  • Use nullok only 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.

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.