SSH keys are the right way to log into a VPS — nobody's arguing that. But a key is still just a file, and files leak. A stolen laptop, a malware infection that scrapes ~/.ssh, or a key copied to a dev machine that later gets compromised, and suddenly whoever has that file has root on your server, no password prompt, no second check. If you want a real second factor — something the attacker needs in addition to the key, not instead of it — Google Authenticator's PAM module is the standard way to bolt TOTP codes onto SSH.

This walks through setting it up on a SkyServer VPS running Ubuntu/Debian or AlmaLinux/Rocky, including the step everyone skips and then locks themselves out over.

Why key-based auth alone isn't enough

Key auth already blocks password-guessing bots — that's most of what hits your SSH port. But it's a single factor: possession of the key file. There's no PIN, no prompt, nothing that lives outside the file itself. If the key is copied, the thief has exactly what you have. Adding TOTP means a valid key gets you to a second prompt, and without a synced authenticator app (or the emergency scratch codes), that's where it stops.

This is different from tools like Fail2Ban, which stops brute-force attempts but does nothing if the attacker already has a valid key. It's also different from just disabling root login — that stops one specific attack path, not a leaked-credential scenario.

What you need before you start

  • Root or sudo access to the VPS
  • A second SSH session you can keep open while you test (critical — explained below)
  • An authenticator app on your phone: Google Authenticator, Authy, or anything that reads standard TOTP QR codes
  • The server's clock in sync (TOTP codes are time-based; more than ~30 seconds of drift and codes stop validating — run timedatectl to confirm NTP sync is active before you go further)

Step 1: Install the PAM module

On Ubuntu or Debian:

sudo apt update
sudo apt install libpam-google-authenticator -y

On AlmaLinux or Rocky Linux, you'll need EPEL first:

sudo dnf install epel-release -y
sudo dnf install google-authenticator -y

Step 2: Generate a TOTP secret for your user

Run this as the user who'll be logging in via SSH — not as root, unless root is genuinely who logs in directly:

google-authenticator

It'll ask a series of yes/no questions. For a standard hosting VPS setup:

PromptRecommended answer
Make tokens time-basedYes
Update the .google_authenticator fileYes
Disallow multiple uses of the same tokenYes
Increase original generation window (clock skew tolerance)Yes
Enable rate-limitingYes

It prints a QR code straight to the terminal — scan it with your authenticator app, then write down the printed emergency scratch codes somewhere that isn't the server itself. Those codes are your way back in if your phone dies or the app gets wiped. This step gets skipped constantly and it's the reason people end up filing a support ticket to get back into their own VPS.

Step 3: Wire it into PAM

Edit /etc/pam.d/sshd and add this line near the top, before the existing @include common-auth or auth substack password-auth line:

auth required pam_google_authenticator.so

If you want key auth plus TOTP (the normal setup, not key-or-password fallback), leave the rest of the file as-is — PAM will chain the checks.

Step 4: Update sshd_config

Open /etc/ssh/sshd_config and set:

ChallengeResponseAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

That AuthenticationMethods line is what actually forces both factors — without it, PAM will prompt for TOTP but SSH won't require it, and anyone with just the key can skip past. On some distros the directive is spelled ChallengeResponseAuthentication, on newer OpenSSH builds it's KbdInteractiveAuthentication — set both if you're not sure which your version reads, it's harmless to have the unused one present.

Restart SSH to apply:

sudo systemctl restart sshd

Step 5: Test in a second session — don't skip this

Before you close your current terminal, open a brand-new SSH connection in a second window. If the config has a typo, this is how you find out without getting locked out of a remote box with no console access. Log in with your key, confirm you're prompted for the six-digit code, enter it, and confirm you land on a shell. Only then close your original session.

If you do get locked out, most hosting VPS management panels (including SkyServer's) let you open a browser-based console/VNC session that bypasses SSH entirely, so you can fix sshd_config and restart the service from there.

Common problems after setup

"Authentication failed" even with a correct-looking code

Almost always clock drift between your phone and the server. TOTP codes are only valid for a ~30 second window. Check server time with timedatectl and make sure NTP sync is enabled and actually running, not just configured.

Prompted for TOTP but the key isn't being checked at all

Means AuthenticationMethods publickey,keyboard-interactive isn't set, or is set but SSH is still reading a cached config — confirm with sshd -T | grep authenticationmethods after restarting the service.

Locked out with no scratch codes and no phone

This is what the emergency console access is for. Once you're in via the VPS panel's console, you can either temporarily comment out the pam_google_authenticator.so line to get back in over SSH, fix the underlying issue, then re-enable it.

Prevention: keep this from becoming a support ticket

  • Store scratch codes in a password manager, not a text file on the same server
  • Set up TOTP for every account that has SSH access, not just root — a compromised low-privilege user is still a foothold
  • Keep NTP sync on and verified; a drifting clock is the single most common cause of "it worked yesterday" tickets
  • Document which authenticator app and which phone the codes are tied to, so a new hire or a phone replacement doesn't turn into a fire drill

Frequently Asked Questions

Does this replace SSH keys, or work alongside them?

Alongside. The setup above uses AuthenticationMethods publickey,keyboard-interactive, which requires both a valid key and a valid TOTP code. Don't swap keys out for password + TOTP unless you have a specific reason — keys are still stronger against automated attacks.

Will this work with SFTP clients like FileZilla or WinSCP?

Only if the client supports keyboard-interactive prompts during the connection — most modern SFTP clients do, but you'll be asked for the TOTP code mid-connection the same way you would in a terminal. Test it before rolling this out to anyone who relies on a GUI client for deployments.

What happens if I lose my phone and don't have the scratch codes?

You'll need out-of-band access to the server — typically the browser-based console/VNC session in your VPS control panel — to comment out the PAM line or regenerate a new secret. This is exactly why scratch codes need to live somewhere other than the server.

Does this slow down automated deployments (CI/CD, cron-triggered SSH)?

Yes, and it will break them if not handled carefully — automated scripts can't answer an interactive TOTP prompt. The usual fix is to exempt specific service accounts or CI IP ranges via a separate Match block in sshd_config, rather than disabling 2FA account-wide.

Can I use this on a shared cPanel hosting account instead of a VPS?

No — this requires root-level PAM and sshd configuration access, which shared hosting doesn't expose. On shared cPanel hosting, cPanel's own built-in two-factor authentication for the control panel login is the equivalent protection available to you.