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.