Adding OTP (One-Time Password) 2FA to SSH logins reduces the impact of stolen passwords or keys by demanding a second factor that changes every few seconds. Requiring an app-generated code in addition to the usual password hardens remote shell access against brute-force attacks and credential reuse.
On Linux servers, OTP-based 2FA commonly relies on PAM (Pluggable Authentication Modules) and the libpam-google-authenticator module. PAM mediates the authentication flow, requesting a password first and then delegating OTP verification to pam_google_authenticator.so using a shared secret stored in each user's /home/USERNAME/.google_authenticator file.
Enabling OTP for SSH modifies both /etc/ssh/sshd_config and /etc/pam.d/sshd, which directly control remote access. Misconfiguration can block new logins, so changes should be tested in an existing session and applied gradually to specific users before rolling out globally. A compatible TOTP app (such as Google Authenticator, Authy, or FreeOTP) and accurate time synchronization between server and phone are required.
Steps to configure OTP 2FA/MFA for SSH authentication:
- Open a terminal session on the SSH server with sudo privileges.
- Install the libpam-google-authenticator module on the server.
$ sudo apt update && sudo apt install --assume-yes libpam-google-authenticator Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease [126 kB] Get:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease [126 kB] Get:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease [126 kB] Get:5 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Packages [1,781 kB] Get:6 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Components [172 kB] Get:7 http://ports.ubuntu.com/ubuntu-ports noble-updates/restricted arm64 Components [212 B] Get:8 http://ports.ubuntu.com/ubuntu-ports noble-updates/universe arm64 Packages [1,467 kB] Get:9 http://ports.ubuntu.com/ubuntu-ports noble-updates/universe arm64 Components [376 kB] Get:10 http://ports.ubuntu.com/ubuntu-ports noble-updates/multiverse arm64 Components [212 B] Get:11 http://ports.ubuntu.com/ubuntu-ports noble-backports/main arm64 Components [3,576 B] Get:12 http://ports.ubuntu.com/ubuntu-ports noble-backports/restricted arm64 Components [216 B] Get:13 http://ports.ubuntu.com/ubuntu-ports noble-backports/universe arm64 Components [10.5 kB] Get:14 http://ports.ubuntu.com/ubuntu-ports noble-backports/multiverse arm64 Components [212 B] Get:15 http://ports.ubuntu.com/ubuntu-ports noble-security/main arm64 Components [18.4 kB] Get:16 http://ports.ubuntu.com/ubuntu-ports noble-security/restricted arm64 Components [212 B] Get:17 http://ports.ubuntu.com/ubuntu-ports noble-security/universe arm64 Components [71.4 kB] Get:18 http://ports.ubuntu.com/ubuntu-ports noble-security/multiverse arm64 Components [208 B] Fetched 4,281 kB in 3s (1,384 kB/s) Reading package lists... Building dependency tree... Reading state information... 10 packages can be upgraded. Run 'apt list --upgradable' to see them. Reading package lists... Building dependency tree... Reading state information... The following additional packages will be installed: libqrencode4 The following NEW packages will be installed: libpam-google-authenticator libqrencode4 0 upgraded, 2 newly installed, 0 to remove and 10 not upgraded. Need to get 71.0 kB of archives. After this operation, 282 kB of additional disk space will be used. Get:1 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 libqrencode4 arm64 4.1.1-1build2 [25.3 kB] Get:2 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 libpam-google-authenticator arm64 20191231-2build1 [45.7 kB] Fetched 71.0 kB in 1s (72.1 kB/s) Selecting previously unselected package libqrencode4:arm64. (Reading database ... (Reading database ... 5% (Reading database ... 10% (Reading database ... 15% (Reading database ... 20% (Reading database ... 25% (Reading database ... 30% (Reading database ... 35% (Reading database ... 40% (Reading database ... 45% (Reading database ... 50% (Reading database ... 55% (Reading database ... 60% (Reading database ... 65% (Reading database ... 70% (Reading database ... 75% (Reading database ... 80% (Reading database ... 85% (Reading database ... 90% (Reading database ... 95% (Reading database ... 100% (Reading database ... 272033 files and directories currently installed.) Preparing to unpack .../libqrencode4_4.1.1-1build2_arm64.deb ... Unpacking libqrencode4:arm64 (4.1.1-1build2) ... Selecting previously unselected package libpam-google-authenticator. Preparing to unpack .../libpam-google-authenticator_20191231-2build1_arm64.deb ... Unpacking libpam-google-authenticator (20191231-2build1) ... Setting up libqrencode4:arm64 (4.1.1-1build2) ... Setting up libpam-google-authenticator (20191231-2build1) ... Processing triggers for man-db (2.12.0-4build2) ... Processing triggers for libc-bin (2.39-0ubuntu8.6) ...
- Open the sshd configuration file in a text editor.
$ sudo vi /etc/ssh/sshd_config
Incorrect changes to /etc/ssh/sshd_config can block new SSH sessions; keep an existing privileged session open while editing.
- Enable keyboard-interactive authentication and scope OTP to a specific account using a Match User block.
KbdInteractiveAuthentication yes Match User otpuser AuthenticationMethods keyboard-interactiveReplace otpuser with the account name that should use OTP 2FA, or omit the Match User block to rely on the default keyboard-interactive method for all users.
- Save and exit the editor after completing the sshd configuration changes.
- Validate the sshd configuration syntax before restarting the service.
$ sudo sshd -t
Resolve any reported errors before restarting; starting sshd with an invalid configuration can terminate remote access.
- Restart the SSH service to apply the new configuration.
$ sudo systemctl restart ssh
- Open the PAM configuration file for sshd in a text editor.
$ sudo vi /etc/pam.d/sshd
- Append entries to require the account password first and then the Google Authenticator OTP.
# Ubuntu/Debian (common-auth already handles the password prompt) @include common-auth auth required pam_google_authenticator.so # Other distros (explicit pam_unix) auth required pam_unix.so no_warn try_first_pass auth required pam_google_authenticator.so
On Ubuntu 24.04, sshd includes
@include common-auth
by default; keep it and place pam_google_authenticator.so immediately after.
- Save and exit the editor after updating the PAM configuration.
- Initialize OTP settings for the target account using the non-interactive setup mode.
$ google-authenticator -t -f -C -d -r 3 -R 30 -w 3 Warning: pasting the following URL into your browser exposes the OTP secret to Google: https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/otpuser@host%3Fsecret%3DK7QDBL3DX4GAM3JMO5F3OGL3XI%26issuer%3Dhost Your new secret key is: K7QDBL3DX4GAM3JMO5F3OGL3XI Your verification code for code 1 is 832362 Your emergency scratch codes are: 37980217 53351616 82177266 97991441 51967920
Store scratch codes offline in a secure location to recover access if the mobile device is unavailable.
If you are logged in as root, run the command as the target account:
$ sudo -u otpuser -H google-authenticator -t -f -C -d -r 3 -R 30 -w 3
The OTP secret file is stored as
~/.google_authenticator
and must be owned by the target account to avoid authentication failures. Fix ownership if needed:
$ sudo chown otpuser:otpuser /home/otpuser/.google_authenticator
Limit access to the OTP secret file:
$ sudo chmod 600 /home/otpuser/.google_authenticator
- Scan the QR code in Google Authenticator or enter the secret manually.

- Confirm the effective SSH authentication settings for the OTP user.
$ sudo sshd -T -C user=otpuser | grep -Ei 'kbdinteractiveauthentication|authenticationmethods' kbdinteractiveauthentication yes authenticationmethods keyboard-interactive
- Open a new terminal or client window and verify the server only offers keyboard-interactive authentication for the OTP user.
$ ssh -vv -o PreferredAuthentications=keyboard-interactive -o PubkeyAuthentication=no -o PasswordAuthentication=no -o BatchMode=yes otpuser@host.example.net exit ##### snipped ##### debug1: Authentications that can continue: keyboard-interactive debug1: No more authentication methods to try. otpuser@host.example.net: Permission denied (keyboard-interactive).
In a real interactive login, the server prompts for the account password followed by the OTP verification code.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
