SSH 2FA with Google Authenticator and Yubikey
About a lifetime ago (5 years), I wrote a tutorial on how to configure my Yubikey for OpenPGP signing, SSH authentication and SSH 2FA. In there, I used the libpam-oath PAM plugin for authentication, but it turns out that had too many problems: users couldn't edit their own 2FA tokens and I had to patch it to avoid forcing 2FA on all users. The latter was merged in the Debian package, but never upstream, and the former was never fixed at all. So I started looking at alternatives and found the Google Authenticator libpam plugin. A priori, it's designed to work with phones and the Google Authenticator app, but there's no reason why it shouldn't work with hardware tokens like the Yubikey. Both use the standard HOTP protocol so it should "just work".
After some fiddling, it turns out I was right and you can authenticate with a Yubikey over SSH. Here's that procedure so you don't have to second-guess it yourself.
Installation
On Debian, the PAM module is shipped in the google-authenticator source package:
apt install libpam-google-authenticator
Then you need to add the module in your PAM stack somewhere. Since I
only use it for SSH, I added this line on top of /etc/pam.d/sshd
:
auth required pam_google_authenticator.so nullok
I also used no_increment_hotp debug
while debugging to avoid having
to renew the token all the time and have more information about
failures in the logs.
Then reload ssh (not sure that's actually necessary):
service ssh reload
Creating or replacing tokens
To create a new key, run this command on the server:
google-authenticator -c
This will prompt you for a bunch of questions. To get them all right, I prefer to just call the right ones on the commandline directly:
google-authenticator --counter-based --qr-mode=NONE --rate-limit=1 --rate-time=30 --emergency-codes=1 --window-size=3
Those are actually the defaults, if my memory serves me right, except
for the --qr-mode
and --emergency-codes
(which can't be disabled
so I only print one). I disable the QR code display because I won't be
using the codes on my phone, but you would obviously keep it if you
want to use the app.
Converting to a Yubikey-compatible secret
Unfortunately, the encoding (base32) produced by the
google-authenticator
command is not compatible with the token
expected by the ykpersonalize
command used to configure the Yubikey
(base16 AKA "hexadecimal", with a fixed 20 bytes length). So you
need a way to convert between the two. I wrote a program called
oath-convert which basically does this:
read base32
add padding
convert to hex
print
Or, in Python:
def convert_b32_b16(data_b32):
remainder = len(data_b32) % 8
if remainder > 0:
# assume 6 chars are missing, the actual padding may vary:
# https://tools.ietf.org/html/rfc3548#section-5
data_b32 += "======"
data_b16 = base64.b32decode(data_b32)
if len(data_b16) < 20:
# pad to 20 bytes
data_b16 += b"\x00" * (20 - len(data_b16))
return binascii.hexlify(data_b16).decode("ascii")
Note that the code assumes a certain token length and will not work correctly for other sizes. To use the program, simply call it with:
head -1 .google_authenticator | oath-convert
Then you paste the output in the prompt:
$ ykpersonalize -1 -o oath-hotp -o append-cr -a
Firmware version 3.4.3 Touch level 1541 Program sequence 2
HMAC key, 20 bytes (40 characters hex) : [SECRET GOES HERE]
Configuration data to be written to key configuration 1:
fixed: m:
uid: n/a
key: h:[SECRET REDACTED]
acc_code: h:000000000000
OATH IMF: h:0
ticket_flags: APPEND_CR|OATH_HOTP
config_flags:
extended_flags:
Commit? (y/n) [n]: y
Note that you must NOT pass the -o oath-hotp8
parameter to the
ykpersonalize
commandline, which we used to do in the Yubikey
howto. That is because Google Authenticator
tokens are shorter: it's less secure, but it's an acceptable tradeoff
considering the plugin is actually maintained. There's actually a
feature request to support 8-digit codes so that limitation might
eventually be fixed as well.
Thanks to the Google Authenticator people and Yubikey people for their support in establishing this procedure.