How to Set Up MFA for SSH Connections on Linux Servers

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
SSH key authentication is more secure than passwords, but a stolen private key file gives an attacker persistent access to every server that trusts that key: with no expiry and no rate limiting. MFA closes this gap: even with the private key, an attacker also needs the rotating TOTP code from the user's authenticator app.
This guide configures Google Authenticator PAM: the most widely deployed SSH MFA method: for both key+TOTP and password+TOTP authentication modes.
Step 1: Install Google Authenticator PAM
Ubuntu / Debian:
sudo apt-get update
sudo apt-get install -y libpam-google-authenticator
RHEL / CentOS / AlmaLinux / Rocky Linux:
sudo dnf install -y epel-release
sudo dnf install -y google-authenticator-libpam
Verify installation:
pamtester sshd $(whoami) open_session # Should return pamtester: successfully authenticated
Step 2: Configure Google Authenticator Per User
Each user who will SSH to the server must run this command as themselves (not as root) to generate their TOTP secret:
google-authenticator
The interactive prompts: answer as follows for a secure default configuration:
Do you want authentication tokens to be time-based? y
# QR code appears here: scan with Google Authenticator, Authy, or any TOTP app
Do you want me to update your ~/.google_authenticator file? y
Do you want to disallow multiple uses of the same authentication token? y
By default, tokens are good for 30 seconds. Do you want to extend it to 4 minutes? n
Do you want to enable rate-limiting? y
The QR code must be scanned before the terminal session ends: it is only displayed once. The wizard also displays emergency scratch codes (one-time backup codes for when the authenticator app is unavailable): store these securely.
The command creates ~/.google_authenticator containing the TOTP secret key and configuration. This file must be readable only by the user:
chmod 600 ~/.google_authenticator
For system administrators setting up MFA for other users: Users must run google-authenticator themselves, or the admin must run it under their account (sudo -u username google-authenticator) and ensure they scan the QR code. The secret cannot be set up without the user's participation.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Step 3: Configure PAM for SSH
Edit /etc/pam.d/sshd to require Google Authenticator:
sudo nano /etc/pam.d/sshd
Add this line: the placement matters:
For key + TOTP (most secure: requires both SSH key and TOTP code): Add at the top of the file, before any existing auth lines:
auth required pam_google_authenticator.so
For password + TOTP (if password authentication is enabled): Add after the existing password auth:
auth required pam_google_authenticator.so
To allow users who have not yet set up Google Authenticator to log in (migration period):
auth required pam_google_authenticator.so nullok
The nullok option allows users without a ~/.google_authenticator file to authenticate without TOTP. Remove nullok once all users have enrolled: after that, any user without the file cannot SSH in.
Step 4: Configure sshd_config
Edit /etc/ssh/sshd_config:
sudo nano /etc/ssh/sshd_config
Required settings for key + TOTP:
# Enable keyboard-interactive authentication (needed for TOTP prompt)
KbdInteractiveAuthentication yes
ChallengeResponseAuthentication yes
# Require BOTH public key AND keyboard-interactive (TOTP)
AuthenticationMethods publickey,keyboard-interactive
# Keep password authentication disabled
PasswordAuthentication no
# Enable PAM (required for Google Authenticator PAM)
UsePAM yes
For password + TOTP (if password login is required):
PasswordAuthentication yes
KbdInteractiveAuthentication yes
AuthenticationMethods keyboard-interactive
UsePAM yes
Restart SSH after changes:
# IMPORTANT: Do not log out of your current session yet
# Open a second SSH connection to verify before closing the first
sudo systemctl restart sshd
Verify in a new terminal window: test the new configuration before closing your existing session. If something is misconfigured, your existing session remains open to fix it.
Step 5: Test and Troubleshoot
Test from a new SSH session:
ssh -v user@server.example.com
The expected authentication flow for key + TOTP:
- SSH key authentication completes silently
- A prompt appears:
Verification code:: enter the 6-digit TOTP code from the authenticator app - Session opens
Common issues:
Issue: TOTP prompt does not appear (goes straight to key-only login)
Cause: AuthenticationMethods is not set correctly, or UsePAM yes is missing
Fix: Verify both settings in sshd_config and restart sshd
Issue: Permission denied even with correct TOTP code
Cause: Clock skew: TOTP codes are time-based and require the server and authenticator device to have synchronized clocks. A difference of more than 90 seconds causes code rejection.
Fix: sudo timedatectl set-ntp true and verify NTP is syncing: timedatectl status
Issue: Users locked out after setup
Cause: nullok not set during migration, user has not run google-authenticator yet
Fix: Temporarily add nullok to the PAM line, allow the user to run google-authenticator, then remove nullok
Exempting specific accounts from MFA (service accounts):
# In /etc/pam.d/sshd: use pam_succeed_if to skip MFA for specific users
auth [success=1 default=ignore] pam_succeed_if.so user ingroup nomfa
auth required pam_google_authenticator.so
Create a nomfa group and add service accounts to it: they authenticate with key only. All other users require key + TOTP.
The bottom line
SSH MFA with Google Authenticator PAM requires five steps: install the PAM module, run google-authenticator as each user to generate TOTP secrets, add auth required pam_google_authenticator.so to /etc/pam.d/sshd, set AuthenticationMethods publickey,keyboard-interactive and UsePAM yes in sshd_config, then restart sshd and test from a new session before closing the existing one. Use nullok during the migration period to avoid locking out users who have not yet enrolled: remove it once all users have set up their authenticator.
Frequently asked questions
How do I add MFA to SSH on Linux?
Install the Google Authenticator PAM module (libpam-google-authenticator on Debian/Ubuntu), run 'google-authenticator' as each user to generate TOTP secrets, add 'auth required pam_google_authenticator.so' to /etc/pam.d/sshd, and set 'AuthenticationMethods publickey,keyboard-interactive' plus 'UsePAM yes' in /etc/ssh/sshd_config. Restart sshd and test from a new session before closing your existing one.
Is SSH key authentication enough without MFA?
SSH key authentication is significantly stronger than passwords, but a stolen private key file provides persistent server access without any expiry or rate limiting. Adding TOTP MFA means an attacker who steals the private key still cannot authenticate without the rotating 6-digit code from the user's authenticator app: closing the stolen-credential attack vector.
How do I enforce SSH key management across multiple Linux servers?
Central management is essential: storing authorized_keys files individually per server creates an ungovernable sprawl. Use one of three approaches: (1) Configure PAM to authenticate against your identity provider (Okta, Entra ID) so SSH access is controlled by your SSO system; (2) Use a bastion host or jump server — all SSH access goes through a single managed host, eliminating direct server access; (3) Use a PAM tool like HashiCorp Vault's SSH secrets engine, which issues short-lived signed certificates instead of long-lived keys: certificates expire automatically, eliminating key sprawl. Whichever approach you use, audit authorized_keys files quarterly for stale entries.
What is an SSH bastion host and how does it improve security?
An SSH bastion (jump server) is the single entry point for all SSH connections to internal servers: external users SSH to the bastion, then from the bastion to target servers. Security benefits: only one public IP needs to be exposed for SSH (all other servers block direct inbound SSH); all SSH sessions are logged and auditable at the bastion; MFA can be enforced at the bastion entry point; access control is centralized (add/remove server access at the bastion, not per-server). AWS Systems Manager Session Manager provides a managed alternative to a bastion host: SSH sessions through SSM require no public IP on target instances and are logged to CloudTrail.
How do I audit which SSH keys have access to my Linux servers?
For each server, check the authorized_keys file for each user account: 'for user in $(cut -d: -f1 /etc/passwd); do echo "$user:"; cat /home/$user/.ssh/authorized_keys 2>/dev/null; done'. Also check /root/.ssh/authorized_keys for root access. For fleet-wide auditing, use Ansible with a task that reads authorized_keys from all hosts and reports to a central location. Red flags: keys without comments (cannot identify the owner), keys for users who no longer work at the company, multiple keys per user without a documented reason, and root authorized_keys entries (prefer sudo over direct root SSH).
How do you handle SSH MFA enrollment for existing users without locking anyone out?
Rolling out SSH MFA to an existing user base requires a migration window where unenrolled users can still access servers while enrolled users get the security benefit of TOTP. The nullok option in the PAM configuration is the mechanism: when present, it allows users without a ~/.google_authenticator file to authenticate without providing a TOTP code. The migration process runs in three phases. Phase one: deploy the PAM configuration with nullok enabled and notify all users that MFA enrollment is mandatory by a deadline two to four weeks out. Provide a step-by-step enrollment guide covering the google-authenticator command, QR code scanning, and scratch code storage. Phase two: during the enrollment window, monitor which users have created their ~/.google_authenticator files. Run a daily check across your fleet: 'ansible all -m shell -a "test -f ~/.google_authenticator && echo enrolled || echo not_enrolled"'. Send reminders to unenrolled users at the two-week, one-week, and two-day marks before the deadline. Phase three: on the deadline date, remove nullok from the PAM configuration. Any user who has not enrolled is blocked from SSH access. Have a support process ready (a ticket queue where IT administrators can assist blocked users in completing enrollment via console access or an alternative channel). For service accounts that must connect without interactive TOTP (automated deployments, monitoring agents), create a dedicated group exempt from MFA requirements using the pam_succeed_if group check pattern and document each exemption with its business justification.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
