I’ve been using the pam_duo module as 2FA for my SSH logins for probably 3+ years, but decided I want to try out simple TOTP-driven 2FA for my server.

A quick skim of various guides on the ‘net, and it comes down to a few config changes.

First, install the PAM module google_authenticator – libpam-google-authenticator on Debian.

Second, enable challenge response in OpenSSH’s config, set the authentication methods to publickey + keyboard-interactive, enable PAM, and disable password based auth. The latter isn’t needed explicitly, since you can do password + TOTP, but I’m quite happy with public key + TOTP.

Next, add a single line to the /etc/pam.d/sshd config – “auth required pam_google_authenticator.so”

Finally, restart sshd.

And yet I spent several hours trying to get this to work. Every time I tried to log in, I’d get the prompt for the verification code, and then the logs would show that PAM authentication failed, and I’d get prompted again. Rinse, repeat until the SSH server gives up on my attempts.

Right as I started putting together an issue on the Github page for PGA, I decided to make my pam.d/sshd configuration as bare-bones as possible. Just the auth line via PGA – no session, no nologin checks, no other auth/account checks. And it worked.

With this working, I started effectively bisecting my config changes, and noticed that I had “auth requisite pam_deny.so” in the config, after the PGA line. It hadn’t caused problems with the pam_duo config, but apparently it was causing problems with pam_google_authenticator. Given that line is a hard PAM failure, it makes sense.. and then I realised that I had configured the pam_duo module to skip the next line when it succeeded ([success=1 default=ignore]), but hadn’t configured the PGA module the same way. Turns out “required” is shorthand for a set of key/value pairs wrapped in []s, and it doesn’t mean “skip the next check”.

Derp.

Now it all works.

 # /etc/ssh/sshd_config
 AuthenticationMethods publickey,keyboard-interactive:pam
 ChallengeResponseAuthentication yes
 PasswordAuthentication no
 PermitEmptyPasswords no
 PermitRootLogin no
 PubkeyAuthentication yes
 UsePAM yes 
# /etc/pam.d/sshd
 auth required pam_google_authenticator.so debug
 account    required     pam_nologin.so
 account include common-account
 session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so close
 session    required     pam_loginuid.so
 session    optional     pam_keyinit.so force revoke
 @include common-session
 session    required     pam_limits.so
 session    required     pam_env.so # [1]
 session    required     pam_env.so user_readenv=1 envfile=/etc/default/locale
 session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so open
Debug logs (sshd set to DEBUG, debug parameter set on the pam_google_authenticator.so line)

 sshd[295785]: debug1: auth2_challenge_start: trying authentication method 'pam' [preauth]
 sshd(pam_google_authenticator)[295366]: debug: start of google_authenticator for "notroot"
 ...
 sshd(pam_google_authenticator)[295366]: Accepted google_authenticator for notroot
 sshd(pam_google_authenticator)[295366]: debug: "/home/notroot/.google_authenticator" written
 sshd(pam_google_authenticator)[295366]: debug: end of google_authenticator for "notroot". Result: Success
 sshd[295364]: error: PAM: Authentication failure for notroot from 
 sshd[295364]: Failed keyboard-interactive/pam for notroot from IP port 51414 ssh2
OpenSSH, Google Authenticator PAM module