How to Configure Two-Factor Authentication (2FA) for SSH on CentOS Stream 9 with Google Authenticator and PAM

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

Why SSH Keys Alone Are Not Enough

Most sysadmins disable password authentication (PasswordAuthentication no) and rely solely on SSH keys. While this is significantly more secure than standard passwords, risks still remain if a developer’s machine gets infected with malware, an unpassphrased id_ed25519 key is stolen, or a private key is accidentally pushed to a public GitHub repository.

An attacker with your private key gains direct access to your server. To prevent this threat, you should implement an additional layer of two-factor authentication (2FA). This requires users to satisfy two conditions to log in: possessing the SSH key on their local machine and providing a real-time 6-digit OTP generated on their phone.

Below are the steps I have deployed on internal CentOS Stream 9 servers using the Google Authenticator PAM module. This mechanism uses the standard TOTP protocol (RFC 6238), fully compatible with any authenticator app like Google Authenticator, Microsoft Authenticator, 1Password, or Authy.

Installing the Google Authenticator PAM Module

The google-authenticator package is not available in the default CentOS Stream 9 repositories; it resides in EPEL (Extra Packages for Enterprise Linux).

Step 1: Install Packages from EPEL

Run the following commands with sudo privileges:

# Install the EPEL repository
sudo dnf install -y epel-release

# Install the Google Authenticator PAM module and QR code library
sudo dnf install -y google-authenticator qrencode-libs

Bước 2: Generate Secret Keys and OTP Codes for Each User

Note: Switch to the specific user account requiring SSH access before running the initialization command. Avoid using the root account to generate keys for regular users.

google-authenticator

The interactive Google Authenticator script will prompt you with five configuration questions:

  • Do you want authentication tokens to be time-based (y/n)? → Type y to enable TOTP (codes refresh every 30 seconds).
  • The screen will display a QR code along with a Secret key and 5 emergency scratch codes. Scan the QR code immediately with your mobile authenticator app, then save the 5 emergency codes securely in Bitwarden or 1Password in case you lose access to your phone.
  • Do you want me to update your "~/.google_authenticator" file (y/n)? → Type y to write the configuration to your home directory.
  • Do you want to disallow multiple uses of the same authentication token (y/n)? → Type y to prevent replay attacks (ensuring each OTP is valid for a single use only).
  • By default, a new token is generated every 30 seconds… (y/n)? → Type n to keep the default window (3 valid tokens, up to ±30 seconds of time skew). Only choose y if severe time synchronization issues occur between the server and your phone.
  • Do you want to enable rate-limiting (y/n)? → Type y to limit failed attempts to a maximum of 3 tries every 30 seconds, protecting against brute-force OTP attacks.

Configuring PAM and the SSH Daemon

After generating the ~/.google_authenticator file, configure SSHD and PAM to enforce OTP entry upon login.

Step 1: Declare the Module in PAM

Open /etc/pam.d/sshd:

sudo vi /etc/pam.d/sshd

Add the following line at the top of the file (before other auth rules):

auth required pam_google_authenticator.so nullok

Pro Tip: The nullok option allows users who haven’t generated an OTP yet to log in using just their SSH key as before. Once all team members have configured their 2FA, remove the nullok parameter to strictly enforce OTP verification for 100% of accounts.

Step 2: Configure OpenSSH on CentOS 9

On CentOS Stream 9 (OpenSSH 8.7+), it is recommended to add configuration snippets under /etc/ssh/sshd_config.d/ rather than editing the main configuration file directly:

sudo vi /etc/ssh/sshd_config.d/2fa.conf

Contents of 2fa.conf:

# Enable keyboard-interactive authentication (to prompt the SSH client for OTP)
KbdInteractiveAuthentication yes

# Enable PAM
UsePAM yes

# Require two-factor authentication: valid Public Key FIRST, followed by OTP
AuthenticationMethods publickey,keyboard-interactive

Setting AuthenticationMethods publickey,keyboard-interactive ensures that if a client does not present a valid SSH key, the server rejects the connection immediately without ever prompting for an OTP.

Step 3: Verify Syntax and Restart SSHD

Always test your configuration files before restarting the service to avoid syntax errors that could break SSHD:

sudo sshd -t

If no errors or warnings are returned, restart the service:

sudo systemctl restart sshd

Crucial: Keep your current SSH session active! Open a new terminal tab on your local machine to test the connection first. If anything is misconfigured, your existing session remains open so you can fix the issue without getting locked out.

Testing the Connection and Monitoring Logs

1. Test Real-World Login

Open a terminal on your local machine and SSH into the server:

ssh username@your-server-ip

The authentication flow will proceed as follows:

  1. The client successfully authenticates with the SSH key.
  2. The server prompts: Verification code:.
  3. Enter the 6-digit code from your authenticator app and press Enter to access the shell.

2. Monitor Authentication Logs

To verify that the PAM module is operating correctly or to troubleshoot failed login attempts, inspect real-time logs using journalctl:

sudo journalctl -u sshd -f

On a successful login, you will see a log entry similar to:

Accepted keyboard-interactive/pam for user from 192.168.1.50 port 52341 ssh2

If an incorrect OTP is entered, the log will output Invalid verification code or Failed keyboard-interactive/pam. You can leverage these log strings to configure Fail2ban to automatically ban IPs after 3 consecutive failed attempts.

3. Recovery from a Lost or Replaced Phone

If you cannot access your authenticator app:

  • Enter one of the five 8-digit Emergency scratch codes saved during setup. Each scratch code can only be used once.
  • Once logged in, remove the old configuration and generate a new QR code:
rm -f ~/.google_authenticator
google-authenticator

This setup takes only 5-10 minutes to complete but significantly elevates your Linux server security, completely neutralizing the risk of unauthorized access via compromised private keys.

Share: