$linuxjunkies
>

How to Add Two-Factor Authentication to SSH

Enforce two-factor authentication on SSH using PAM and Google Authenticator TOTP, with YubiKey alternatives and per-user exemption patterns for automation accounts.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • SSH server (openssh-server) installed and running
  • sudo or root access on the target server
  • An RFC 6238-compatible authenticator app installed on your phone (Aegis, Google Authenticator, Bitwarden, etc.)
  • An active SSH session to the server before making changes, to avoid lockout

Password-based SSH is a single point of failure. Two-factor authentication (2FA) adds a second layer: even if a private key or password is stolen, an attacker still needs a time-based one-time password (TOTP) from your authenticator app or a hardware token. This guide walks through enforcing 2FA on SSH using PAM and libpam-google-authenticator, with notes on YubiKey as an alternative.

How It Works

OpenSSH delegates authentication to PAM (Pluggable Authentication Modules) when configured to do so. The pam_google_authenticator.so module intercepts the login flow and prompts for a TOTP code generated by any RFC 6238-compatible app (Google Authenticator, Aegis, Authy, Bitwarden, etc.). The flow you'll build is:

  • Public-key authentication passes (or password, depending on your policy)
  • PAM then demands the TOTP code
  • Both must succeed for the session to open

You can optionally require only TOTP (no password), but the most secure setup chains a key with TOTP.

Step 1 — Install the PAM Module

Debian / Ubuntu

sudo apt update
sudo apt install libpam-google-authenticator

Fedora / RHEL 9 / Rocky Linux

sudo dnf install google-authenticator qrencode

On RHEL-family systems the package is in the EPEL repository. Enable it first if you haven't:

sudo dnf install epel-release

Arch Linux

sudo pacman -S libpam-google-authenticator

Step 2 — Run the Setup as Each User

Every user who will log in via SSH must run this command as themselves, not as root. It writes a secret key and scratch codes to ~/.google_authenticator.

google-authenticator

Answer the interactive prompts deliberately:

  • Time-based tokens? — Yes. TOTP is far more common and better supported than HOTP.
  • Update .google_authenticator? — Yes.
  • Disallow reuse? — Yes. Prevents replay attacks.
  • Rate limiting? — Yes. Limits brute-force attempts.
  • Permit 30-second window skew? — Answer based on whether your clocks are NTP-synced (yes = strict, no = allows ±1 window). Tight clocks → choose no for stricter security.

The command prints a QR code in the terminal and an emergency scratch codes block. Save the five scratch codes offline immediately — they are single-use recovery tokens.

Scan the QR code with your authenticator app or manually enter the secret key shown beneath it.

Step 3 — Configure PAM for SSH

Edit the PAM configuration for the SSH daemon. The file path differs by distro.

Debian / Ubuntu

sudo nano /etc/pam.d/sshd

Add this line at the end of the file (or after the @include common-auth line):

auth required pam_google_authenticator.so

Fedora / RHEL / Rocky

sudo nano /etc/pam.d/sshd

Add the same line. On these systems the file already contains several auth lines; append yours after them:

auth required pam_google_authenticator.so

Arch Linux

sudo nano /etc/pam.d/sshd

Add the same line at the end.

The required control flag means PAM will always evaluate this module and fail the entire authentication if it returns failure — even if other modules already passed. This is intentional.

Step 4 — Configure sshd

Open /etc/ssh/sshd_config. You need to set (or confirm) three directives:

sudo nano /etc/ssh/sshd_config

Set or update these lines:

ChallengeResponseAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

The AuthenticationMethods line enforces that both a public key and keyboard-interactive (which triggers PAM/TOTP) are required. If you want TOTP to supplement passwords instead of keys, use keyboard-interactive alone — but key + TOTP is the stronger policy.

Note: On OpenSSH 8.7+ (Fedora 35+, Ubuntu 22.04+) ChallengeResponseAuthentication was renamed to KbdInteractiveAuthentication. Check your version:

ssh -V

If your version is 8.7 or later, use:

KbdInteractiveAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

Both directive names are accepted in many versions for backward compatibility, but use the current name on modern systems to avoid deprecation warnings.

Step 5 — Reload sshd

Always reload, not restart, from an existing session. A configuration error in a restart could lock you out.

sudo systemctl reload sshd

Keep your current SSH session open while testing from a second terminal. Do not close it until you have confirmed login works.

Step 6 — Test the Login

From a second terminal, connect to the server:

ssh -v user@yourserver

You should see your key accepted, then a Verification code: prompt. Enter the 6-digit TOTP from your app. A successful login confirms the chain is working. If login fails, your original session is still open to troubleshoot.

YubiKey as an Alternative

A YubiKey can replace or supplement TOTP. The simplest integration uses the YubiKey's TOTP slot (configured via the YubiKey Authenticator app) — it works with the exact same PAM setup above, because the YubiKey just generates TOTP codes.

For FIDO2/WebAuthn-based SSH authentication (stronger, phishing-resistant), OpenSSH 8.2+ supports sk-ecdsa-sha2-nistp256 and sk-ed25519 key types natively. Generate a resident key directly:

ssh-keygen -t ed25519-sk -O resident -C "yubikey-ssh"

This approach binds the private key to the hardware token entirely — no PAM module needed, and no separate TOTP step. It is the most robust option when hardware tokens are available to all users.

Verification

After a successful test login, confirm PAM is actually enforcing the module by checking auth logs:

Debian / Ubuntu

sudo journalctl -u ssh --since "5 minutes ago"

Fedora / RHEL / Rocky / Arch

sudo journalctl -u sshd --since "5 minutes ago"

Look for lines referencing pam_google_authenticator and Accepted keyboard-interactive. A failed TOTP attempt logs Failed keyboard-interactive.

Troubleshooting

  • "Permission denied (publickey)" with no TOTP prompt: AuthenticationMethods is set but KbdInteractiveAuthentication/ChallengeResponseAuthentication is not yes, or sshd was not reloaded.
  • TOTP codes rejected immediately: Clock skew between server and phone. Run timedatectl status on the server to check NTP sync. Phones should also have automatic time enabled. Re-run google-authenticator and answer yes to the 30-second window prompt for tolerance.
  • Root lockout: If you enforce TOTP for root, ensure root has run google-authenticator and that PermitRootLogin is set appropriately. Better practice: disable root SSH entirely (PermitRootLogin no) and use sudo from a 2FA-protected regular account.
  • Service accounts / automation: Automated SSH (rsync, Ansible, CI/CD) breaks if TOTP is enforced globally. Use Match User blocks in sshd_config to exempt specific accounts:
    Match User deploy
        AuthenticationMethods publickey
  • Lost phone / codes: Use one of your scratch codes to log in, then re-run google-authenticator to generate a new secret.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can I require TOTP without also requiring a public key?
Yes. Set AuthenticationMethods to keyboard-interactive (not publickey,keyboard-interactive) in sshd_config. This prompts for TOTP but not a key, which is weaker than the combined approach.
How do I exempt a user or service account from TOTP?
Use a Match User block in sshd_config: 'Match User deploy\n AuthenticationMethods publickey'. This overrides the global AuthenticationMethods for that account only.
My TOTP codes are always rejected — what's wrong?
Almost always clock skew. Run 'timedatectl status' on the server and confirm NTP synchronization is active. Also verify your phone's time is set automatically. Re-running google-authenticator and enabling the ±1 window tolerance helps in marginal cases.
Is the YubiKey FIDO2 method more secure than TOTP?
Yes. FIDO2 resident keys stored on a YubiKey are phishing-resistant and bind authentication to the physical hardware entirely, with no shared secret that could be extracted from ~/.google_authenticator.
What happens if I lose my phone and my scratch codes?
You'll need out-of-band access to the server — physical console, cloud provider web console, or a recovery user that bypasses TOTP. This is why storing scratch codes offline (printed or in a password manager) is non-negotiable.

Related guides