# Two-factor authentication for SSH (TOTP on Ubuntu/Debian)
Source: https://vpsdime.com/knowledgebase/technical-questions/ssh-2fa-totp
Last reviewed: 2026-07-02
Summary: Add authenticator-app codes to SSH logins with libpam-google-authenticator, when it's genuinely worth it (keeping password auth, or belt-and-braces over keys), the full setup (PAM line, KbdInteractiveAuthentication, AuthenticationMethods policies), the anti-lockout discipline with the Console as the safety net, and the clock-skew and scratch-code operational notes.

## What this is

Requiring a **six-digit authenticator code** at SSH login, the same TOTP apps (Google Authenticator, Authy, 1Password) you use for [your account's 2FA](https://vpsdime.com/knowledgebase/client-area/account/two-factor-overview), enforced by the server through `libpam-google-authenticator`.

First, the honest positioning, because this upgrade is often oversold:

- **If you still allow password logins**, TOTP is a genuinely big upgrade: a leaked or guessed password stops being enough. (Though the bigger upgrade remains [switching to keys](https://vpsdime.com/knowledgebase/getting-started/setting-up-ssh-keys), and our [network already blunts password brute-force](https://vpsdime.com/knowledgebase/technical-questions/ssh-bruteforce-firewall).)
- **If you're already key-only**, a passphrase-protected key is effectively two factors (the key file + the passphrase), so TOTP adds a third, mostly protecting against a *stolen, unlocked* key. Worth it for high-stakes boxes and compliance checklists; optional for a hobby server.

The strong combination this guide builds: **key + code** (`publickey,keyboard-interactive`), something you have, something you know, something that rotates.

## Setup

Everything below on Ubuntu/Debian; same module on RHEL-family via the `google-authenticator` package (EPEL).

**1. Install the PAM module:**

```
apt install libpam-google-authenticator
```

**2. Enroll the login user.** As the user who logs in over SSH (not necessarily root), run:

```
google-authenticator
```

Answer **y** to time-based tokens, scan the QR with your authenticator app, and **save the emergency scratch codes somewhere safe off the server**, they are your login of last resort if the phone is gone. The sensible defaults for the remaining prompts: update the file (y), disallow token reuse (y), and enable rate limiting (y).

**3. Tell PAM to require the code.** Add to the top section of `/etc/pam.d/sshd`:

```
auth required pam_google_authenticator.so
```

(For a gradual rollout, `auth required pam_google_authenticator.so nullok` lets users who haven't enrolled yet log in without a code, remove `nullok` once everyone's enrolled.)

**4. Tell SSH to ask.** In `/etc/ssh/sshd_config`:

```
KbdInteractiveAuthentication yes
```

(older configs call it `ChallengeResponseAuthentication`), and set the **policy** with `AuthenticationMethods`:

- Key **plus** code (recommended): `AuthenticationMethods publickey,keyboard-interactive`
- Password **plus** code: `AuthenticationMethods password,keyboard-interactive` (make sure `PasswordAuthentication yes` in this case)

**5. Restart and test, without closing your session:**

```
systemctl restart ssh
```

Then, **from a second terminal**, log in fresh: key (or password) first, then `Verification code:` prompts for the app's six digits.

## The anti-lockout discipline

PAM misconfiguration is the classic way to lock yourself out of SSH entirely, so the rules from every [firewall](https://vpsdime.com/knowledgebase/troubleshooting/allow-ssh-in-common-firewalls) and [key](https://vpsdime.com/knowledgebase/getting-started/setting-up-ssh-keys) guide apply doubly: **keep your working session open until a second terminal proves the new flow**, keep the scratch codes off-server, and know the safety net, the **[Console](https://vpsdime.com/knowledgebase/client-area/services/managing-your-vps)** in your client area doesn't authenticate through SSH or PAM's sshd stack, so even a fully botched config is a fix-it-from-the-console incident, not a rebuild.

## Operational notes

- **TOTP lives and dies by the clock.** Codes are time-based; if the server's clock drifts, valid codes get rejected. Verify `timedatectl` shows `System clock synchronized: yes` (systemd-timesyncd handles it by default), before blaming the app.
- **Each user enrolls separately** (`google-authenticator` run as that user; the secret lives in their `~/.google_authenticator`). Automation and deploy accounts are usually better served by [keys without TOTP](https://vpsdime.com/knowledgebase/getting-started/setting-up-ssh-keys), scope `AuthenticationMethods` per-user with a `Match User` block if you mix.
- **Know what it protects**: SSH logins, that's all. Your web apps, panels, and [everything else exposed](https://vpsdime.com/knowledgebase/getting-started/securing-public-services) have their own login doors.

## Still need help?

You can [open a support ticket](https://vpsdime.com/open-ticket/). So we can help on the first reply, it's worth mentioning:

- the VPS hostname or IP,
- where the flow fails (PAM, sshd config, a rejected code), with the exact prompt or error,
- whether the Console still gets you in.

## Related questions

- "How do I add two-factor authentication to SSH?"
- "How do I set up google-authenticator on Ubuntu?"
- "Should I use SSH keys, TOTP, or both?"
- "Why are my SSH verification codes rejected (clock skew)?"
- "What happens if I lose my phone (scratch codes)?"
- "Can some users skip the code requirement (nullok, Match User)?"
