How to Set Up Two-Factor Authentication (2FA) for SSH on Ubuntu with Google Authenticator

Ubuntu tutorial - IT technology blog
Ubuntu tutorial - IT technology blog

Relying solely on SSH keys is not always foolproof. If your local machine gets infected with malware or someone accidentally pushes a private key to GitHub, your server becomes immediately vulnerable. Enabling two-factor authentication (2FA) directly at the SSH layer is the fastest and most reliable way to mitigate this risk.

Quick Start: Set Up SSH 2FA in 5 Minutes

The steps below apply directly to Ubuntu 20.04, 22.04, and 24.04 LTS.

Step 1: Install the Google Authenticator PAM Module

Update the package list and install the corresponding PAM library:

sudo apt update
sudo apt install libpam-google-authenticator -y

Step 2: Generate the Secret Key and QR Code

Run the following command as the specific user you want to configure 2FA for (do not use sudo):

google-authenticator

The system will prompt you with five interactive configuration questions. Answer them as follows:

  • Do you want authentication tokens to be time-based (y/n)? → Select y to use time-based TOTP codes.
  • The terminal will display a QR code, a Secret Key, and five emergency scratch codes (8 digits each). Open Google Authenticator, 2FAS, or Aegis to scan the QR code immediately. Make sure to back up the five emergency scratch codes in a password manager.
  • Do you want me to update your “~/.google_authenticator” file (y/n)? → Select y to write the configuration file.
  • Do you want to disallow multiple uses of the same authentication token (y/n)? → Select y to prevent replay attacks. Each 6-digit code can only be used once within a 30-second window.
  • Increase the time-skew window (y/n)? → Select n. This keeps the verification window at the default (30 seconds), preventing attackers from having extra time to guess codes.
  • Enable rate-limiting (y/n)? → Select y to restrict logins to a maximum of 3 failed attempts per 30 seconds.

Step 3: Configure PAM (Pluggable Authentication Modules)

Open the SSH daemon PAM configuration file:

sudo nano /etc/pam.d/sshd

Append the following line to the end of the file:

auth required pam_google_authenticator.so nullok

Tip: The nullok parameter is crucial. It allows users who have not yet run google-authenticator to log in normally using their password or key, preventing unintended lockouts during phased rollouts.

Step 4: Update the SSH Daemon Configuration

Next, open the OpenSSH configuration file:

sudo nano /etc/ssh/sshd_config

Locate and set the following two directives to yes:

KbdInteractiveAuthentication yes
UsePAM yes

(Note: On Ubuntu releases prior to 22.04, this directive is named ChallengeResponseAuthentication yes).

Step 5: Restart the SSH Service and Verify

sudo systemctl restart ssh

Important warning: Do not close your current terminal tab. Open a new terminal window and test logging in:

ssh username@your_server_ip

The server will prompt you for a Verification code: before granting shell access.

How It Works

SSH 2FA authentication operates across two layers:

  1. OpenSSH Server (sshd): Accepts the incoming connection and delegates the authentication challenge to PAM via KbdInteractiveAuthentication.
  2. PAM Module (libpam-google-authenticator): Reads the ~/.google_authenticator file in the user’s home directory. The module calculates the OTP using the TOTP algorithm (RFC 6238) by hashing the static Secret Key with the current timestamp (30-second window). If the 6 digits match the code displayed in the authenticator app, the SSH session is authorized.

Advanced Configuration: Enforce Both SSH Key + 2FA Code

By default, when authentication via SSH key succeeds, OpenSSH skips the OTP prompt. To achieve maximum security, you can require users to satisfy both requirements: both a valid Private Key and a valid TOTP code.

First, open /etc/pam.d/sshd. Comment out the @include common-auth line so the server does not ask for the Ubuntu account password when an SSH key is present:

# @include common-auth
auth required pam_google_authenticator.so

Next, edit /etc/ssh/sshd_config to define the required authentication sequence:

PubkeyAuthentication yes
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Test the SSH configuration syntax before applying changes to prevent syntax errors:

sudo sshd -t && sudo systemctl restart ssh

Now, the client must first successfully authenticate with the SSH key. Only then will the server display the Verification code: prompt. If either factor is missing or incorrect, the connection is immediately terminated.

Bypassing 2FA for Local Networks or VPNs

If you want to enforce 2FA when connecting from the public Internet, but skip the OTP requirement when connected via corporate WireGuard or OpenVPN, use a Match block in /etc/ssh/sshd_config:

# Require Key + 2FA by default for all external connections
AuthenticationMethods publickey,keyboard-interactive

# Bypass OTP for internal VPN IP ranges (SSH Key only)
Match Address 10.8.0.0/24,192.168.1.0/24
    AuthenticationMethods publickey

Best Practices & Preventing Lockouts

  • Always maintain an active backup session: When modifying /etc/pam.d/ or /etc/ssh/sshd_config, keep an active root SSH session open. If the new tab fails to log in, you can fix configuration issues immediately from the existing session.
  • Synchronize system time via NTP: TOTP relies on 30-second time intervals. A time drift exceeding 30 seconds between the server and your mobile device will cause valid OTP codes to be rejected. Check time synchronization with:
timedatectl status
# Ensure: "NTP service: active" and "System clock synchronized: yes"
  • Save Emergency Scratch Codes: If you lose your phone or your authenticator app fails, these 5 backup codes are your only lifeline. Each code can only be used once; store them in 1Password, Bitwarden, or your team vault.
  • Resetting 2FA if locked out: If you lose your phone and have no backup codes left, access your VPS web console (such as AWS EC2 Serial Console, DigitalOcean Droplet Console, or Proxmox) and delete the configuration file:
sudo rm /home/username/.google_authenticator
Share: