Password-based SSH authentication is a well-known attack vector, and even key-based authentication can be compromised if a private key is stolen. Adding Time-Based One-Time Password (TOTP) two-factor authentication creates a second layer of defense that is independent of the key. This tutorial shows you how to configure the Google Authenticator PAM module on RHEL 9 to require both an SSH key and a one-time code on every login.

Prerequisites

  • RHEL 9 server with SSH access and root or sudo privileges
  • A smartphone with an authenticator app installed (Google Authenticator, Authy, or any TOTP-compatible app)
  • EPEL repository enabled on the server
  • An existing SSH key pair configured for your user account (recommended before enabling 2FA)

Step 1 — Install the Google Authenticator PAM Module

The google-authenticator-libpam package is available from the EPEL repository. Enable EPEL if you have not already done so, then install the package:

dnf install -y epel-release
dnf install -y google-authenticator

Verify the PAM module was installed:

ls /usr/lib64/security/pam_google_authenticator.so

Step 2 — Generate the Authenticator Secret for Each User

Each user who needs 2FA must run the google-authenticator command from their own account. This generates a secret key, a QR code, and a set of emergency scratch codes. Run this as the user who will log in via SSH (not as root, unless root SSH login is required):

google-authenticator

The interactive wizard asks several questions. The recommended answers for most setups are:

# Do you want authentication tokens to be time-based? → y
# Update your ~/.google_authenticator file? → y
# Disallow multiple uses of the same token? → y
# Increase the time window? → n  (keeps security tighter)
# Enable rate-limiting? → y

A QR code will be displayed in the terminal. Scan it with your authenticator app. Also save the emergency scratch codes printed below the QR code in a secure, offline location — these are your recovery codes if you lose your phone.

Step 3 — Configure PAM for SSH

Edit the SSH PAM configuration file to require the Google Authenticator module. Open /etc/pam.d/sshd and add the following line near the top, after the auth substack password-auth line:

# Back up the original file first
cp /etc/pam.d/sshd /etc/pam.d/sshd.bak

# Add the google-authenticator line
# Insert after: auth       substack     password-auth
# The file should contain this line:
auth       required     pam_google_authenticator.so nullok

The nullok option means users who have not yet run google-authenticator can still log in with just their key. Remove nullok once all users have enrolled to make 2FA mandatory for everyone.

Your /etc/pam.d/sshd relevant section should look like this:

#%PAM-1.0
auth       required     pam_sepermit.so
auth       required     pam_google_authenticator.so nullok
auth       substack     password-auth
auth       include      postlogin

Step 4 — Configure sshd_config

Edit /etc/ssh/sshd_config to enable challenge-response authentication and require both a public key and the one-time code:

# Back up sshd_config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# Edit the following directives (add or modify):
# KbdInteractiveAuthentication yes          (replaces ChallengeResponseAuthentication on RHEL 9)
# AuthenticationMethods publickey,keyboard-interactive

# Use sed to apply the changes:
sed -i 's/^#?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication yes/' /etc/ssh/sshd_config

# Add AuthenticationMethods if not present
grep -q "^AuthenticationMethods" /etc/ssh/sshd_config || 
  echo "AuthenticationMethods publickey,keyboard-interactive" >> /etc/ssh/sshd_config

Validate the configuration before restarting:

sshd -t && echo "Config OK"

Restart SSH to apply the changes. Keep your current session open while testing from a second terminal:

systemctl restart sshd

Step 5 — Test the Two-Factor Login

Open a new terminal window and attempt to SSH into the server. With AuthenticationMethods publickey,keyboard-interactive, the login flow is: SSH key authentication first, then a prompt for the one-time code.

ssh -v youruser@your-server-ip

After the SSH key is accepted you should see:

Authenticated with partial success.
Verification code:

Open your authenticator app, find the entry for this server, and type the 6-digit code. The code refreshes every 30 seconds — if it expires while you’re typing, wait for the next one. A successful login confirms 2FA is working correctly.

Step 6 — Harden and Maintain the Setup

Once all users have enrolled, remove the nullok option from /etc/pam.d/sshd to enforce 2FA for everyone. Also consider additional hardening steps:

# Protect the .google_authenticator files
chmod 600 ~/.google_authenticator

# Disable root SSH login entirely (best practice)
sed -i 's/^#?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config

# Optionally restrict SSH to specific users
echo "AllowUsers youruser anotheradmin" >> /etc/ssh/sshd_config

systemctl restart sshd

Conclusion

You now have TOTP two-factor authentication enforced for SSH logins on RHEL 9. Every login requires both a valid SSH private key and a time-based one-time code from an authenticator app, making unauthorized access significantly harder even if a private key is compromised. Remember to store your emergency scratch codes securely and to enroll all administrative users before making 2FA mandatory.

Next steps: How to Audit Linux Security with Lynis on RHEL 9, How to Configure nftables Firewall on RHEL 9, and How to Configure Fail2Ban on RHEL 9.