Secure your SSH connections by adding TOTP-based two-factor authentication using Google Authenticator and PAM modules for an additional layer of protection beyond passwords and keys.
Prerequisites
- Root or sudo access to the server
- Smartphone with authenticator app
- Basic SSH access configured
What this solves
SSH two-factor authentication adds an extra security layer by requiring both your password/key and a time-based token from your phone. This prevents unauthorized access even if your SSH credentials are compromised, making it essential for servers handling sensitive data or exposed to the internet.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest security updates.
sudo apt update && sudo apt upgrade -y
Install Google Authenticator PAM module
Install the PAM module that handles TOTP token validation for SSH authentication.
sudo apt install -y libpam-google-authenticator qrencode
Generate TOTP secret for your user
Run the Google Authenticator setup as the user who will use SSH 2FA. This creates a secret key and QR code.
google-authenticator
Answer the prompts as follows:
- "Do you want authentication tokens to be time-based?" → y
- "Do you want me to update your "/home/username/.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..." → n
- "Do you want to enable rate-limiting?" → y
Scan QR code with authenticator app
Use your smartphone's authenticator app to scan the QR code displayed in the terminal. Popular options include Google Authenticator, Authy, or Microsoft Authenticator.
The QR code contains your secret key and server information. Your app will start generating 6-digit codes that change every 30 seconds.
Configure PAM for SSH authentication
Add the Google Authenticator PAM module to SSH authentication. Edit the SSH PAM configuration file.
sudo nano /etc/pam.d/sshd
Add this line at the top of the file, before other auth lines:
auth required pam_google_authenticator.so
Configure SSH daemon for 2FA
Modify the SSH daemon configuration to enable both password/key authentication and challenge-response authentication.
sudo nano /etc/ssh/sshd_config
Find and modify these lines (or add them if they don't exist):
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
UsePAM yes
For password-based authentication with 2FA, use this instead:
ChallengeResponseAuthentication yes
AuthenticationMethods password,keyboard-interactive
UsePAM yes
PasswordAuthentication yes
Restart SSH service
Restart the SSH daemon to apply the configuration changes.
sudo systemctl restart sshd
sudo systemctl status sshd
Verify the service restarted without errors before proceeding.
Set up 2FA for additional users
Each user who needs SSH 2FA must run the google-authenticator setup individually. Switch to each user account and repeat the setup process.
sudo su - username
google-authenticator
Each user gets their own secret key and emergency codes. Users cannot share TOTP tokens.
Test SSH 2FA login
Test the two-factor authentication from a different terminal or machine. Keep your current SSH session open as backup.
ssh username@your-server-ip
You should see prompts for:
- Your SSH key passphrase (if using key-based auth) or password
- "Verification code:" prompt for your 6-digit TOTP code
Enter the current code from your authenticator app. The login should succeed only with both credentials.
Verify your setup
Check that SSH 2FA is working correctly with these verification steps:
# Check SSH daemon configuration
sudo sshd -T | grep -E 'challengeresponseauthentication|authenticationmethods|usepam'
Verify PAM configuration
grep google_authenticator /etc/pam.d/sshd
Check user's 2FA file exists
ls -la ~/.google_authenticator
Test authentication in dry-run mode
sudo ssh -o PreferredAuthentications=keyboard-interactive username@localhost
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| "Verification code:" never appears | ChallengeResponseAuthentication disabled | Set ChallengeResponseAuthentication yes in sshd_config |
| Codes always rejected | Server time drift | Sync time with sudo ntpdate -s time.nist.gov |
| Locked out completely | No emergency codes saved | Remove ~/.google_authenticator via console access |
| Only asks for password | AuthenticationMethods not set | Add AuthenticationMethods line to sshd_config |
| PAM authentication failure | Wrong PAM module path | Verify pam_google_authenticator.so exists in /lib/security/ |
| SSH key bypasses 2FA | Single auth method configured | Use AuthenticationMethods publickey,keyboard-interactive |
Configure backup access methods
Set up emergency access to prevent lockouts when 2FA fails.
Save emergency scratch codes
During initial setup, Google Authenticator provides emergency scratch codes. Store these securely offline.
# View existing codes (if you missed them)
head -1 ~/.google_authenticator
Configure console access exemption
Allow local console login without 2FA for emergency access. Edit the PAM configuration:
sudo nano /etc/pam.d/sshd
Modify the Google Authenticator line to skip 2FA for local connections:
auth [success=1 default=ignore] pam_access.so accessfile=/etc/security/access-local.conf
auth required pam_google_authenticator.so
Create backup user account
Maintain one admin account without 2FA for emergencies, accessible only from specific IP addresses.
sudo useradd -m -s /bin/bash emergency-admin
sudo usermod -aG sudo emergency-admin
sudo passwd emergency-admin
Restrict this account in /etc/ssh/sshd_config:
Match User emergency-admin
AuthenticationMethods password
AllowUsers emergency-admin
PermitRootLogin no
Advanced configuration options
Customize token validation window
Adjust time tolerance for TOTP codes to handle clock drift. Edit each user's configuration:
nano ~/.google_authenticator
Add these options to the first line after the secret key:
# Add to secret key line: " WINDOW_SIZE=3
This allows codes from 1.5 minutes before/after current time
Enable grace login period
Allow a grace period for new 2FA setups. Modify the PAM line:
auth required pam_google_authenticator.so grace_period=86400
This gives users 24 hours to set up their authenticator apps after their first login.
Configure per-user 2FA settings
Create a script to batch-configure 2FA for multiple users with consistent settings:
#!/bin/bash
USER=$1
if [ -z "$USER" ]; then
echo "Usage: $0 username"
exit 1
fi
sudo -u $USER google-authenticator --time-based --disallow-reuse --force --rate-limit=3 --rate-time=30 --window-size=3
echo "2FA setup complete for user: $USER"
echo "Emergency codes saved to /home/$USER/.google_authenticator"
sudo chmod +x /usr/local/bin/setup-user-2fa.sh
sudo /usr/local/bin/setup-user-2fa.sh newuser
Monitor and maintain 2FA
Set up monitoring to track 2FA usage and failures:
# Monitor SSH authentication attempts
sudo tail -f /var/log/auth.log | grep sshd
Check for 2FA-related errors
sudo grep "google_authenticator" /var/log/auth.log
Monitor failed login attempts
sudo grep "Failed password" /var/log/auth.log
Consider integrating with existing monitoring systems to alert on repeated 2FA failures, which may indicate attack attempts.
Next steps
- Configure SSH port forwarding and tunneling for secure connections
- Set up SSH bastion host with jump server configuration for secure network access
- Configure SSH certificate authentication with certificate authority
- Implement SSH session recording and audit logging for compliance
- Configure LDAP authentication for SSH with two-factor authentication
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Global variables
BACKUP_DIR="/tmp/ssh-2fa-backup-$(date +%s)"
CLEANUP_NEEDED=false
# Print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Usage message
usage() {
echo "Usage: $0 [--user USERNAME] [--auth-method key|password]"
echo ""
echo "Options:"
echo " --user USERNAME Setup 2FA for specific user (default: current user)"
echo " --auth-method METHOD Authentication method: 'key' or 'password' (default: key)"
echo ""
echo "Example:"
echo " $0 --user alice --auth-method key"
exit 1
}
# Cleanup function for rollback
cleanup() {
if [ "$CLEANUP_NEEDED" = true ] && [ -d "$BACKUP_DIR" ]; then
print_warning "Rolling back changes..."
[ -f "$BACKUP_DIR/sshd_config" ] && cp "$BACKUP_DIR/sshd_config" /etc/ssh/sshd_config
[ -f "$BACKUP_DIR/sshd_pam" ] && cp "$BACKUP_DIR/sshd_pam" /etc/pam.d/sshd
systemctl restart sshd || true
print_warning "Rollback completed. Backup files are in: $BACKUP_DIR"
fi
}
# Set trap for cleanup on error
trap cleanup ERR
# Check if running as root or with sudo
check_privileges() {
if [ "$EUID" -ne 0 ]; then
print_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution and package manager
detect_distro() {
if [ ! -f /etc/os-release ]; then
print_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PKG_UPGRADE="apt upgrade -y"
GA_PACKAGE="libpam-google-authenticator"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf update -y"
GA_PACKAGE="google-authenticator"
# Enable EPEL for older versions
if ! dnf repolist | grep -q epel; then
dnf install -y epel-release || true
fi
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf update -y"
GA_PACKAGE="google-authenticator"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum update -y"
GA_PACKAGE="google-authenticator"
yum install -y epel-release || true
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
}
# Parse command line arguments
parse_args() {
TARGET_USER=""
AUTH_METHOD="key"
while [[ $# -gt 0 ]]; do
case $1 in
--user)
TARGET_USER="$2"
shift 2
;;
--auth-method)
AUTH_METHOD="$2"
if [[ "$AUTH_METHOD" != "key" && "$AUTH_METHOD" != "password" ]]; then
print_error "Invalid auth method. Use 'key' or 'password'"
usage
fi
shift 2
;;
-h|--help)
usage
;;
*)
print_error "Unknown option: $1"
usage
;;
esac
done
# If no user specified, use SUDO_USER or current user
if [ -z "$TARGET_USER" ]; then
TARGET_USER="${SUDO_USER:-$(whoami)}"
fi
# Validate user exists
if ! id "$TARGET_USER" &>/dev/null; then
print_error "User '$TARGET_USER' does not exist"
exit 1
fi
}
# Create backup directory
create_backup() {
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
CLEANUP_NEEDED=true
# Backup SSH configuration files
cp /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config"
cp /etc/pam.d/sshd "$BACKUP_DIR/sshd_pam"
print_status "Configuration backed up to: $BACKUP_DIR"
}
# Main installation steps
main() {
parse_args "$@"
check_privileges
detect_distro
create_backup
print_status "[1/8] Updating system packages..."
$PKG_UPDATE
$PKG_UPGRADE
print_status "[2/8] Installing Google Authenticator PAM module..."
$PKG_INSTALL $GA_PACKAGE qrencode
print_status "[3/8] Configuring PAM for SSH authentication..."
if ! grep -q "pam_google_authenticator.so" /etc/pam.d/sshd; then
sed -i '1i auth required pam_google_authenticator.so' /etc/pam.d/sshd
fi
print_status "[4/8] Configuring SSH daemon for 2FA..."
# Enable required SSH settings
sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/^#*UsePAM.*/UsePAM yes/' /etc/ssh/sshd_config
# Set authentication method based on user choice
if [ "$AUTH_METHOD" = "key" ]; then
if grep -q "^AuthenticationMethods" /etc/ssh/sshd_config; then
sed -i 's/^AuthenticationMethods.*/AuthenticationMethods publickey,keyboard-interactive/' /etc/ssh/sshd_config
else
echo "AuthenticationMethods publickey,keyboard-interactive" >> /etc/ssh/sshd_config
fi
else
if grep -q "^AuthenticationMethods" /etc/ssh/sshd_config; then
sed -i 's/^AuthenticationMethods.*/AuthenticationMethods password,keyboard-interactive/' /etc/ssh/sshd_config
else
echo "AuthenticationMethods password,keyboard-interactive" >> /etc/ssh/sshd_config
fi
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
fi
print_status "[5/8] Testing SSH configuration..."
if ! sshd -t; then
print_error "SSH configuration test failed"
exit 1
fi
print_status "[6/8] Restarting SSH service..."
systemctl restart sshd
systemctl status sshd --no-pager -l
print_status "[7/8] Setting up Google Authenticator for user: $TARGET_USER..."
USER_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
print_warning "Switching to user '$TARGET_USER' to configure Google Authenticator..."
print_warning "Please answer the prompts as follows:"
echo " - Time-based tokens: y"
echo " - Update file: y"
echo " - Disallow multiple uses: y"
echo " - Increase window: n"
echo " - Rate limiting: y"
echo ""
print_warning "IMPORTANT: Save the emergency scratch codes shown!"
echo ""
# Run google-authenticator as the target user
su - "$TARGET_USER" -c 'google-authenticator'
# Ensure proper permissions on the authenticator file
if [ -f "$USER_HOME/.google_authenticator" ]; then
chown "$TARGET_USER:$TARGET_USER" "$USER_HOME/.google_authenticator"
chmod 400 "$USER_HOME/.google_authenticator"
fi
print_status "[8/8] Verifying configuration..."
# Verify SSH daemon configuration
echo "SSH Configuration:"
sshd -T | grep -E 'challengeresponseauthentication|authenticationmethods|usepam|passwordauthentication' || true
echo ""
# Verify PAM configuration
echo "PAM Configuration:"
grep google_authenticator /etc/pam.d/sshd || print_warning "Google Authenticator not found in PAM config"
echo ""
# Verify user's 2FA file
if [ -f "$USER_HOME/.google_authenticator" ]; then
print_status "2FA file created successfully for user: $TARGET_USER"
else
print_warning "2FA file not found. User may need to run 'google-authenticator' manually"
fi
print_status "SSH Two-Factor Authentication setup completed successfully!"
echo ""
print_warning "IMPORTANT NEXT STEPS:"
echo "1. Keep this SSH session open as backup"
echo "2. Test SSH 2FA login from another terminal/machine"
echo "3. You will need both your SSH key/password AND the 6-digit code from your authenticator app"
echo "4. Emergency scratch codes are your backup if you lose your phone"
echo ""
print_warning "Test command: ssh $TARGET_USER@$(hostname -I | awk '{print $1}')"
# Don't cleanup on success
CLEANUP_NEEDED=false
}
main "$@"
Review the script before running. Execute with: bash install.sh