You need a second factor
You are managing a Fedora system and you realize your password is the only thing standing between you and an intruder. Passwords get reused. Passwords get leaked in databases. You want to add a second factor so that even if the password is compromised, the account stays locked. You need Time-based One-Time Passwords (TOTP). You want to use a standard app like Aegis, FreeOTP, or Google Authenticator to generate codes that change every 30 seconds.
This setup uses google-authenticator-libpam. The package provides the PAM module that checks the codes and the CLI tool that generates the secrets. It works for SSH logins, local console logins, and sudo. It does not require a proprietary cloud service. The verification happens locally on your machine.
What's actually happening
Linux authentication runs through Pluggable Authentication Modules (PAM). PAM is the security desk for your system. When you log in, PAM checks a stack of rules defined in /etc/pam.d/. By default, the stack checks your password against /etc/shadow. If the password matches, you get access.
TOTP adds a second check to that stack. The pam_google_authenticator.so module generates a secret key for your user. Your phone app stores that same secret. Both your computer and your phone use the current time to calculate a six-digit code. When you log in, PAM asks for the code, calculates the expected value, and compares it. If they match, PAM grants access.
Think of the password as your ID badge and the TOTP code as a daily pass. The ID badge proves who you are. The daily pass proves you are there right now. You need both to enter.
The configuration lives in two places. The PAM stack in /etc/pam.d/ controls what checks run. The SSH daemon config in /etc/ssh/sshd_config controls how SSH presents those checks to the client. You must configure both.
Edit files in /etc/. Never edit files in /usr/lib/. The files in /usr/lib/ ship with packages and get overwritten on updates. Your changes in /etc/ persist.
Install the PAM module
Install the package that provides the PAM module and the setup tool.
sudo dnf install google-authenticator-libpam
# Installs the PAM module for TOTP verification and the CLI tool for key generation
Generate a secret key for your user
Run the generator as the user you want to protect. Do not run this as root. The secret file lands in the user's home directory, and permissions matter.
google-authenticator
# Runs the interactive wizard. Must be run as the target user, not root
The wizard asks a series of questions. Answer them carefully. The defaults are usually correct, but understand what you are enabling.
Do you want authentication tokens to be time-based (y/n) y
# Selects TOTP. Time-based codes expire every 30 seconds. Counter-based codes are for offline use.
The tool displays a QR code in the terminal. Scan it with your TOTP app. The app now has the secret.
Do you want me to update your "/home/user/.google_authenticator" file (y/n) y
# Writes the secret and configuration flags to the user's home directory
Do you want to disallow reuse of authentication tokens (y/n) y
# Prevents replay attacks. Once a code is used, it cannot be used again, even within the time window
By default, tokens are good for 30 seconds and one extra token is allowed before and after the current time... (y/n) y
# Allows for slight clock drift between the server and the phone. Keeps the window at roughly 75 seconds total
If the computer that you are logging into isn't hardened against brute-force login attempts, you can enable rate-limiting... (y/n) y
# Limits login attempts to 3 per 30 seconds. Stops automated guessing scripts
The wizard prints emergency scratch codes. Save these immediately. Print them or store them in a password manager. If you lose your phone, these are your only way to log in. Do not skip this step.
The file ~/.google_authenticator contains the secret, the scratch codes, and the flags. The permissions are set to 600 automatically. Only the user can read this file. If the permissions are wrong, PAM will refuse to use the file for security reasons.
Enable 2FA for SSH logins
SSH needs to know how to ask for the TOTP code. Modern OpenSSH uses AuthenticationMethods to enforce multi-factor flows. You want to require a public key first, then the TOTP code. This is the secure standard. Password-only logins with TOTP are weaker because the password can still be brute-forced.
Edit the SSH daemon configuration.
sudo nano /etc/ssh/sshd_config
# Opens the SSH config for editing. Use your preferred editor.
Find the authentication section. Add or modify these lines.
ChallengeResponseAuthentication yes
# Enables the mechanism that allows PAM to prompt for the TOTP code
AuthenticationMethods publickey,keyboard-interactive
# Requires both a valid SSH key AND a successful TOTP check. Order matters.
If ChallengeResponseAuthentication is commented out, uncomment it. If AuthenticationMethods exists, replace it. If you leave PasswordAuthentication yes without AuthenticationMethods, users can still log in with just a password and TOTP, bypassing the key requirement.
Check the configuration syntax before restarting. A syntax error will kill the SSH service.
sudo sshd -t
# Validates the config file. Returns nothing on success. Prints errors on failure.
Edit the PAM stack for SSH. Add the TOTP module near the top of the auth section.
sudo nano /etc/pam.d/sshd
# Opens the PAM config for SSH.
Add this line before other auth lines.
auth required pam_google_authenticator.so
# Tells PAM to require the TOTP check for SSH authentication
Restart the SSH daemon to apply changes.
sudo systemctl restart sshd
# Reloads the SSH service with the new config
Test from a second terminal window before closing your current session. If the config is wrong, you will lose access.
ssh user@your-server
# Tests the login flow. Do not close your active session until this succeeds.
Enable 2FA for local console and sudo
SSH is only one entry point. Local logins and sudo use a different PAM stack. If you want 2FA everywhere, you must edit system-auth.
Edit the system authentication file.
sudo nano /etc/pam.d/system-auth
# Opens the PAM stack used by login, sudo, and graphical sessions
Add the TOTP line near the top of the auth section.
auth required pam_google_authenticator.so
# Adds TOTP requirement to all local authentications including sudo
Restart is not required for PAM changes. The next login triggers the check.
Be aware of the impact. Adding this line means sudo will ask for the TOTP code every time. This adds security but adds friction. If you only want 2FA for logins and not for every sudo, skip this step.
For graphical logins via GDM, the stack is separate. Edit /etc/pam.d/gdm-password if you need 2FA at the desktop login screen.
Snapshot the system before editing system-auth. A typo here can prevent all local logins, including recovery.
Verify it worked
Open a new SSH connection. The prompt should ask for your TOTP code after verifying your key.
Verification code:
# Enter the 6-digit code from your app
If the code is correct, you get a shell. If the code is wrong, you get denied.
Check the logs if something fails. The journal shows exactly why PAM rejected the attempt.
journalctl -xeu sshd
# Shows SSH logs with explanatory text. Jump to the end to see recent failures.
Look for pam_google_authenticator messages. Common errors include time drift or permission issues.
text
Jan 15 10:23:45 server sshd[1234]: pam_google_authenticator(sshd:auth): Verification failed for user 'admin'
# Indicates the code was wrong or the file is unreadable
Run journalctl -t setroubleshoot if SELinux is blocking access. SELinux denials appear here with a one-line summary. Fix the context before disabling SELinux.
Common pitfalls and what the error looks like
Time synchronization is critical. TOTP relies on the system clock. If your server clock drifts, codes will fail even if they are correct. Ensure chronyd is running.
systemctl status chronyd
# Checks if the time sync service is active. TOTP breaks if this is stopped.
If you see Permission denied (publickey,keyboard-interactive), check AuthenticationMethods. The client might not support keyboard-interactive, or the server config is rejecting the method. Verify the line in sshd_config.
If you get Authentication failed immediately, check the PAM stack order. The pam_google_authenticator.so line must be in the auth section. If it is in account or session, it will not prompt for the code.
Running google-authenticator as root creates the file in /root/.google_authenticator. This does not protect regular users. Each user must run the tool separately.
If you lose your TOTP device, use a scratch code. Scratch codes are single-use. Once used, they are marked as consumed in the file. If you run out of scratch codes and lose your device, you must access the system via a console or recovery mode to disable 2FA or generate a new file.
Recovery via TTY: Press Ctrl+Alt+F3 to open a virtual console. Log in as root if allowed. Edit /etc/pam.d/sshd to comment out the TOTP line. Restart SSH. Log in and regenerate the user's key.
When to use this vs alternatives
Use google-authenticator-libpam when you want TOTP support for SSH and console logins with a standard PAM module that works with any authenticator app.
Use hardware tokens like YubiKey when you need phishing resistance and want to avoid typing codes entirely.
Use SSH keys alone when you control the client machines and do not need a second factor for shared accounts.
Use system-auth integration when you want 2FA for sudo and local logins, not just SSH.
Use nullok in the PAM config only when rolling out 2FA gradually and you need to allow users without keys to log in temporarily.