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:

  1. Open a terminal session on the SSH server with sudo privileges.
  2. 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) ...
  3. 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.

  4. Enable keyboard-interactive authentication and scope OTP to a specific account using a Match User block.
    KbdInteractiveAuthentication yes
    
    Match User otpuser
        AuthenticationMethods keyboard-interactive

    Replace 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.

  5. Save and exit the editor after completing the sshd configuration changes.
  6. 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.

  7. Restart the SSH service to apply the new configuration.
    $ sudo systemctl restart ssh
  8. Open the PAM configuration file for sshd in a text editor.
    $ sudo vi /etc/pam.d/sshd
  9. 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.

  10. Save and exit the editor after updating the PAM configuration.
  11. 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
  12. Scan the QR code in Google Authenticator or enter the secret manually.
  13. 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
  14. 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.