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