To set up SSH two-factor authentication (2FA) on Ubuntu 24.04, require an SSH key as the first factor and a time-based one-time password (TOTP) as the second factor. This tutorial uses Ubuntu's libpam-google-authenticator package, OpenSSH keyboard-interactive authentication, explicit rollback files, and a second-session test so you do not close your only working administrative connection before MFA is proven.
Raff Technologies is used as the Ubuntu VM platform in the saved test workflow. The resulting authentication sequence is:
SSH private key -> PAM TOTP challenge -> shell
This is both SSH 2FA and SSH MFA: the key is a possession factor and the rotating TOTP code is a separate possession factor generated from another secret. Password authentication is disabled in the OpenSSH layer for this design.
Ubuntu 24.04 uses the OpenSSH 9.6p1 base line. On amd64, the current Noble security package verified on September 8, 2026 is 1:9.6p1-3ubuntu13.19; Ubuntu package revisions can advance without changing the upstream 9.6p1 base shown by ssh -V. Ubuntu 24.04 currently provides libpam-google-authenticator version 20191231-2build1.
The original SSH key + TOTP workflow was tested on a Raff Ubuntu 24.04 VM with 1 vCPU and 2 GB RAM. OpenSSH, PAM, Ubuntu package versions, and Google Authenticator PAM behavior were documentation-reviewed for this refresh; no new full machine retest is claimed.
Prerequisites:
- A Raff Linux VM running Ubuntu 24.04
- A non-root administrator account with sudo privileges
- Working SSH key authentication for every interactive administrator who must retain access
- A TOTP-compatible authenticator application
- A tested out-of-band console or another recovery path
- Awareness of any automation or service accounts that use SSH non-interactively
Scope: this tutorial configures SSH key + TOTP. It does not configure password + TOTP. If automation accounts rely on unattended SSH, do not enforce the policy globally until you have designed and tested an explicit exception or separate access path.
Step 1 — Verify SSH key access and create rollback copies
Keep the current SSH session open for the entire change.
From a second terminal, confirm your SSH key already works:
ssh your-user@your-server-ip
On the server, check the active user, SSH service, OpenSSH package, and effective SSH port:
whoami systemctl is-active ssh ssh -V 2>&1 dpkg-query -W -f='${Version}\n' openssh-server sudo sshd -T | awk '/^port / {print $2}'
Back up the PAM and OpenSSH files before changing them:
sudo cp --preserve=all /etc/pam.d/sshd /etc/pam.d/sshd.pre-mfa sudo cp --preserve=all /etc/ssh/sshd_config /etc/ssh/sshd_config.pre-mfa sudo install -d -m 700 /root/ssh-mfa-backup sudo cp -a /etc/ssh/sshd_config.d /root/ssh-mfa-backup/sshd_config.d
Verify the rollback copies:
sudo ls -l /etc/pam.d/sshd.pre-mfa /etc/ssh/sshd_config.pre-mfa sudo test -d /root/ssh-mfa-backup/sshd_config.d \ && echo 'SSH drop-in backup exists'
Do not proceed if the second key-authenticated SSH session cannot connect or you do not have a usable recovery path.
Verify: SSH key login should work in a second session and the PAM, main SSH configuration, and drop-in directory should all have rollback copies.
Step 2 — Install the Google Authenticator PAM module
Update APT metadata and install the Ubuntu package:
sudo apt update sudo apt install -y libpam-google-authenticator
Check the installed package version:
dpkg-query -W -f='${Package} ${Version}\n' libpam-google-authenticator
Ubuntu 24.04 Noble currently provides:
libpam-google-authenticator 20191231-2build1
Find the PAM module without assuming a CPU architecture-specific library path:
find /usr/lib -type f -name pam_google_authenticator.so -print
The package also installs the google-authenticator enrollment command:
command -v google-authenticator
Verify: the package should be installed, google-authenticator should be available, and pam_google_authenticator.so should exist under /usr/lib.
Step 3 — Enroll the administrator in TOTP
Run enrollment as the same non-root user who will log in over SSH:
google-authenticator -t -d -r 3 -R 30 -w 3 -e 5
These options configure:
-t: time-based OTP rather than HOTP-d: disallow reuse of a TOTP token-r 3 -R 30: limit authentication attempts to 3 every 30 seconds-w 3: accept the previous, current, or next 30-second TOTP window-e 5: generate five emergency codes
Scan the displayed QR code with your authenticator application and verify the generated code when prompted. Store emergency codes somewhere protected and separate from the VM.
The command writes the user's TOTP secret and settings to:
~/.google_authenticator
Set the expected permissions explicitly:
chmod 600 ~/.google_authenticator
Verify ownership and mode without reading the secret:
stat -c '%U %G %a %n' ~/.google_authenticator
Check time synchronization because TOTP depends on reasonably accurate clocks:
timedatectl show -p NTPSynchronized --value
Expected output is normally:
yes
The PAM module requires the per-user secret file unless nullok is used. This tutorial intentionally does not use nullok because the final state requires TOTP for enrolled interactive administrators.
Verify: the authenticator should generate matching TOTP codes, .google_authenticator should be owned by the login user with mode 600, and the server clock should be synchronized.
Step 4 — Enroll every interactive administrator before enforcement
Each human administrator should create a separate TOTP secret under their own account:
google-authenticator -t -d -r 3 -R 30 -w 3 -e 5 chmod 600 ~/.google_authenticator
For each administrator, verify that a secret file exists without displaying its contents:
sudo stat -c '%U %a %n' /home/USERNAME/.google_authenticator
Do not share one TOTP secret between multiple people. Individual enrollment preserves user-level accountability and lets you rotate one person's second factor without affecting everyone else.
Inventory non-human SSH accounts before enforcement:
sudo awk -F: '$3 >= 1000 && $7 !~ /(nologin|false)$/ {print $1, $6, $7}' /etc/passwd
This command is only a starting point; service accounts can use different UID ranges or shells. Review actual SSH automation before changing the global policy.
Verify: every human SSH administrator who must retain access should have an individual TOTP secret, and unattended SSH users should be identified before enforcement.
Step 5 — Configure PAM for SSH key plus TOTP
Ubuntu's default /etc/pam.d/sshd normally includes the standard password authentication stack through:
@include common-auth
For the specific SSH key + TOTP flow in this tutorial, leaving that line active can cause keyboard-interactive PAM authentication to request the Unix password as well as the TOTP code. That would create a different three-step flow.
Edit the SSH PAM file:
sudoedit /etc/pam.d/sshd
Comment the normal password-auth include and add the Google Authenticator module near the top:
# Standard Un*x authentication. # @include common-auth auth required pam_google_authenticator.so
Leave the existing account, session, and password-management sections unchanged.
Verify only the relevant lines:
sudo grep -nE 'common-auth|pam_google_authenticator' /etc/pam.d/sshd
Do not add nullok. The module documentation states that nullok permits a user without a secret file to pass this PAM module during rollout, which is not the mandatory-2FA result intended here.
Verify: @include common-auth should be commented only in /etc/pam.d/sshd, while auth required pam_google_authenticator.so remains active.
Step 6 — Configure OpenSSH to require key plus PAM TOTP
Ubuntu's /etc/ssh/sshd_config includes /etc/ssh/sshd_config.d/*.conf near the start. OpenSSH generally uses the first obtained value for these single-valued directives, so use an early drop-in for the MFA policy:
sudo tee /etc/ssh/sshd_config.d/00-raff-ssh-mfa.conf > /dev/null <<'EOF' PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication yes UsePAM yes AuthenticationMethods publickey,keyboard-interactive:pam PermitRootLogin no EOF
The required authentication chain is:
publickey,keyboard-interactive:pam
OpenSSH's AuthenticationMethods directive requires every comma-separated method in the selected list to succeed. Appending :pam to keyboard-interactive explicitly selects PAM as the keyboard-interactive device.
Use KbdInteractiveAuthentication for new configuration. ChallengeResponseAuthentication is a deprecated alias.
Validate syntax before reloading SSH:
sudo sshd -t
No output means the syntax passed.
Inspect the effective authentication policy:
sudo sshd -T | grep -E \ '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|usepam|authenticationmethods|permitrootlogin) '
Expected values include:
pubkeyauthentication yes passwordauthentication no kbdinteractiveauthentication yes usepam yes authenticationmethods publickey,keyboard-interactive:pam permitrootlogin no
If those values differ, inspect all matching directives before reloading:
sudo grep -RniE \ 'PubkeyAuthentication|PasswordAuthentication|KbdInteractiveAuthentication|UsePAM|AuthenticationMethods|PermitRootLogin' \ /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
Verify: sshd -t should return no error and sshd -T should show the intended key-plus-PAM authentication chain.
Step 7 — Reload SSH while keeping the recovery session open
Do not close the original working SSH connection.
Reload OpenSSH:
sudo systemctl reload ssh
Confirm the service remains active:
systemctl is-active ssh
Re-check syntax and effective methods after reload:
sudo sshd -t sudo sshd -T | grep -E \ '^(passwordauthentication|kbdinteractiveauthentication|authenticationmethods) '
If the service or policy is wrong, use the still-authenticated recovery session to roll back before making another login attempt.
Verify: SSH should remain active and the effective policy should still require publickey,keyboard-interactive:pam.
Step 8 — Test SSH key plus TOTP from a new session
From a separate terminal, connect with verbose authentication output:
ssh -v your-user@your-server-ip
Expected sequence:
- The SSH public key is accepted.
- OpenSSH reports partial authentication success or continues to keyboard-interactive.
- PAM asks for a verification code.
- You enter the current TOTP code.
- The shell opens without a Unix password prompt.
After login, confirm the identity:
whoami hostname
From the original recovery session, inspect recent SSH events:
sudo journalctl -u ssh --since '-10 minutes' --no-pager | tail -n 80
Ubuntu may also record authentication details in /var/log/auth.log:
sudo tail -n 80 /var/log/auth.log 2>/dev/null || true
A successful test must require both the private key and a valid TOTP code. Key-only access should not complete authentication, and possession of a TOTP code without the matching SSH private key should not be enough.
Verify: a new SSH session should open only after the SSH key and TOTP challenge both succeed, with no Unix password prompt.
Step 9 — Verify recovery before closing the original session
Confirm your out-of-band recovery path still works before closing the first SSH session. If your recovery policy permits consuming an emergency code for testing, test one from a controlled second session; emergency codes are single-use.
Keep these rollback commands available in the original authenticated session:
sudo cp --preserve=all /etc/pam.d/sshd.pre-mfa /etc/pam.d/sshd sudo cp --preserve=all /etc/ssh/sshd_config.pre-mfa /etc/ssh/sshd_config sudo rm -rf /etc/ssh/sshd_config.d sudo cp -a /root/ssh-mfa-backup/sshd_config.d /etc/ssh/sshd_config.d sudo sshd -t sudo systemctl reload ssh
Run rollback only from an already authenticated SSH or console session. Do not close the working recovery connection until a separate MFA login has succeeded.
Verify: a separate key-plus-TOTP SSH session should work and you should have a usable recovery path before ending the original session.
Step 10 — Verify SSH 2FA end to end
Run the final server checks:
printf 'OpenSSH package:\n' dpkg-query -W -f='${Version}\n' openssh-server printf '\nSSH syntax:\n' sudo sshd -t && echo 'sshd config OK' printf '\nEffective authentication policy:\n' sudo sshd -T | grep -E \ '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|usepam|authenticationmethods|permitrootlogin) ' printf '\nTOTP secret permissions:\n' stat -c '%U %a %n' ~/.google_authenticator printf '\nSSH service:\n' systemctl is-active ssh
From a new client terminal, complete one more real login:
ssh your-user@your-server-ip
End-to-end verification is complete when:
- the SSH public key succeeds as the first factor;
- a TOTP prompt is mandatory as the second factor;
- no Unix password prompt appears;
PasswordAuthenticationis disabled;KbdInteractiveAuthenticationandUsePAMare enabled;AuthenticationMethodsispublickey,keyboard-interactive:pam;- root SSH login is disabled;
pam_google_authenticator.sois required in/etc/pam.d/sshd;.google_authenticatorbelongs to the user with mode600;- a second MFA-protected session works; and
- the original recovery session is kept open until the new login path is proven.
