When a single key isn't enough
You just finished migrating your home server to Fedora. You disabled password logins, generated an Ed25519 key pair, and felt secure. Then you read a thread about a compromised key pair or a brute-force attempt that guessed a weak passphrase. A single factor is a single point of failure. Adding a time-based one-time password to your SSH workflow closes that gap. The setup takes ten minutes and survives reboots, package updates, and sleep cycles.
How PAM and SSH negotiate trust
SSH authentication relies on Pluggable Authentication Modules. PAM handles the actual verification steps. When you add two-factor authentication, you are telling PAM to check two things before handing over a shell. First, it validates your SSH key or password. Second, it asks for a six-digit code generated by an algorithm that syncs with your phone. The algorithm is TOTP. It uses a shared secret and the current Unix timestamp to generate a code that expires every thirty seconds. Think of it like a hotel key card that only works for the current hour. If someone steals your key card, they still cannot enter without the current hour code.
Fedora ships with the pam_google_authenticator module in the base repositories. The module plugs into the SSH daemon through /etc/pam.d/sshd. You will also need to adjust the SSH daemon configuration to tell it to request both factors. The order of operations matters. PAM reads top to bottom. SSH reads its configuration files in a specific precedence chain. Get the order wrong and the daemon will either skip the second factor or refuse to start.
Run systemctl status sshd before you change anything. Note the current state and the active configuration path. You will need that baseline if the service fails to restart.
Install and generate your TOTP secret
Install the authenticator package and the PAM library. The package provides the setup wizard and the shared object that PAM loads.
sudo dnf install -y google-authenticator pam
# --refresh forces dnf to check for updated metadata before installing
# google-authenticator provides the TOTP generator and CLI wizard
# pam ensures the authentication stack has the required shared libraries
Run the setup wizard as the user who will log in. Do not run it as root. The wizard writes the secret to the user's home directory and sets restrictive permissions automatically.
google-authenticator
# Runs the interactive TOTP provisioning wizard for the current user
# Generates a base32 secret, QR code, and emergency scratch codes
# Writes output to ~/.google_authenticator with mode 0600
The wizard asks a series of yes or no questions. Accept the defaults for time-based tokens, file updates, disallowing repeated tokens, and rate limiting. The thirty-second window tolerance accounts for minor clock drift between your phone and the server. Scan the displayed QR code with Aegis, FreeOTP, or your preferred authenticator app. Write down the emergency scratch codes and store them offline. You will need those codes if your phone dies or loses network access.
Wire PAM into the SSH daemon
Open the PAM configuration file for SSH. You will add a single line to the authentication stack. Place it at the top of the auth section. PAM evaluates lines sequentially. Putting the TOTP check first ensures the daemon requests the code after the initial method completes, depending on how you configure AuthenticationMethods.
sudo nano /etc/pam.d/sshd
# Opens the PAM stack for the SSH daemon in a terminal editor
# Always edit files in /etc/ rather than /usr/lib/ to preserve package updates
# Changes here survive dnf upgrades and system reboots
Add this line before any existing auth directives:
auth required pam_google_authenticator.so nullok
# required means the login fails if this module returns an error
# pam_google_authenticator.so loads the TOTP verification logic
# nullok allows login to proceed if the user has not yet generated a secret
Save the file. Do not change the permissions. The file must remain owned by root with mode 0644. PAM refuses to load world-writable configuration files.
Tell sshd to demand two factors
Edit the main SSH daemon configuration. You can also place a drop-in file under /etc/ssh/sshd_config.d/ to keep your changes separate from the package defaults. Drop-ins are easier to audit and remove later.
sudo nano /etc/ssh/sshd_config.d/2fa.conf
# Creates a drop-in configuration file for SSH
# Drop-ins in sshd_config.d/ are read after the main config file
# This keeps custom rules isolated from upstream package updates
Add the following directives:
ChallengeResponseAuthentication yes
# Enables keyboard-interactive mode for PAM prompts
# Required for TOTP to appear after the initial authentication step
AuthenticationMethods publickey,keyboard-interactive
# Forces SSH to require both an SSH key AND a TOTP code
# The comma means both methods must succeed in sequence
The AuthenticationMethods directive uses a strict syntax. A space between methods means either one works. A comma means both are mandatory. If you only want TOTP without an SSH key, change the line to AuthenticationMethods keyboard-interactive and set PasswordAuthentication no. The comma syntax is the standard for defense in depth.
Restart the daemon to apply the changes.
sudo systemctl restart sshd
# Reloads the SSH daemon with the new PAM and config rules
# systemctl restart stops and starts the service cleanly
# Always verify the service state immediately after restarting
Check the service status right after the restart. A failed restart usually points to a syntax error in the configuration or a missing PAM module.
sudo systemctl status sshd
# Shows the current state, recent log lines, and active configuration
# Look for Active: active (running) and zero failed restarts
# Read the journal excerpt if the service shows a degraded state
Verify the lock works
Open a second terminal window. Do not close your current session. Test the login from a different machine or a new terminal tab. If the new session fails, you still have the original connection to fix the configuration.
ssh your-user@your-server
# Initiates a fresh SSH connection to test the new authentication flow
# The client will first validate your SSH key, then prompt for the TOTP
# Keep this session open until you confirm the second factor works
You should see a prompt that looks like this:
Verification code:
Enter the six-digit code from your authenticator app. The session should drop you into a shell. If you see Permission denied (publickey,keyboard-interactive), the daemon rejected one of the factors. Check the server logs before guessing.
sudo journalctl -xeu sshd
# -x adds explanatory hints to journal entries
# -e jumps to the end of the log buffer
# -u sshd filters output to the SSH daemon unit
# Read the actual error before modifying configuration again
Common pitfalls and exact error messages
The most frequent mistake is misordering the PAM stack. If you place pam_google_authenticator.so after pam_unix.so, the daemon may authenticate the password or key and skip the second factor entirely. PAM stops evaluating when it hits a sufficient success condition. Keep the TOTP module at the top of the auth section.
Another common trap is the AuthenticationMethods syntax. A space instead of a comma changes the logic from AND to OR. The daemon will accept either the key or the code, which defeats the purpose of two-factor authentication. Run sshd -T | grep authenticationmethods to verify the effective configuration. The output must show a comma-separated list.
sshd -T | grep authenticationmethods
# Dumps the effective runtime configuration without restarting the service
# Useful for verifying that drop-ins and overrides are applied correctly
# Run this as root to see the full resolved configuration tree
If you lock yourself out, you can recover using the emergency scratch codes. Each code works exactly once. If you exhaust the scratch codes and lose access to your phone, you will need console access to temporarily comment out the PAM line or remove the AuthenticationMethods directive. Physical access or a cloud provider's rescue console is your fallback. Always test 2FA in a secondary terminal before closing your primary session.
Choose your authentication strategy
Use TOTP with SSH keys when you want strong defense against stolen credentials without carrying hardware. Use password-only with TOTP when you are managing legacy systems that cannot handle key-based workflows. Use hardware security keys when you are protecting high-value infrastructure and can tolerate the cost and physical dependency. Stay on password-only authentication only when you are running isolated development machines behind a strict firewall.
Where to go next
- How to Set Up SSH Key-Based Authentication on Fedora
- How to Set Password Expiration Policies on Fedora
- How to Use Access Control Lists (ACLs) on Fedora
Test the second factor before you walk away. A locked-out server is easy to fix with a backup terminal. It is much harder to fix from a coffee shop.