Wednesday, October 30, 2013

How to use Google Authenticator with OpenBSD, OpenSSH, and OpenVPN--and why you might not want to

I thought that Google Authenticator might be a quick and easy two-factor authentication solution for VPN access to my personal network, so I did some Google searches to see if that were so.  I found quite a few sources describing how to set it up with systems that use Linux Pluggable Authentication Modules (PAM), but very little about using it with BSD Authentication on OpenBSD.

The most promising link I came across was to an implementation of Google Authentication for OpenBSD that was last updated in early 2013, based on Google's PAM code, but I couldn't get it to work.  It compiled and installed, and the googleauth code for generating a secret (and a very insecure way of generating a QR code to use to import it into the Google Authenticator application) worked fine, but I couldn't successfully use it for console login, OpenSSH login, or OpenVPN login.

I also found the standard OpenBSD port for openvpn_bsdauth, which compiled, installed, and worked successfully for password authentication by adding these lines to my OpenVPN configuration:
script-security 2
tmp-dir <path to dir writable only by _openvpn user>
auth-user-pass-verify /usr/local/libexec/openvpn_bsdauth via-file

This also requires that the authenticating user be put into the _openvpnusers group.

I was unable to get the via-env method to work, however.

I next tried the standard OpenBSD port of login_oath, which implements the OATH toolkit, which uses the same time-based TOTP protocol that Google Authenticator uses.  This turned out to do the trick.  Once installed, you create a secret key that the server authentication will check against and store it in your home directory (one thing I liked about googleauth is that it stores the shared secret in a system directory to which the user doesn't have access; better still is the suggestion of keeping the secrets on an auth server as totp-cgi does).  The documentation recommends creating the secret (which the user doesn't need to know except for the initial configuration of the Google Authenticator client application) by doing:
openssl rand -hex 20 > ~/.totp-key
I then needed to convert this from hex to base32, which is simple enough to do with the method the documentation recommends, which is using the perl module Convert::Base32 (OpenBSD port p5-Convert-Base32) and a short script like:
use Convert::Base32;
open (FILE, "/home/vpnuser/.totp-key");
$secret = <FILE>;
close (FILE);
$code = pack ('H*', $secret);
print encode_base32($code)."\n";
The resulting code can be manually entered into Google Authenticator.

To use Google Authenticator as a login method, I updated the login class for the user I wanted to use in /etc/login.conf so that its last two lines were:
This allows either Google Authenticator or password authentication at the console, but only Google Authenticator via OpenSSH or OpenVPN as I configured them.  Instead of using "-totp" you can also use "-totp-and-pwd" which requires the entry of both your Google Authenticator code and your password (in that order, with a slash in between them) in order to authenticate.

For OpenSSH, I added the following lines to my sshd_config:
Match User <vpnuser>
     PasswordAuthentication yes
     AuthenticationMethods publickey,password:bsdauth
I don't allow password authentication at all for other users; for this user, an SSH public key must first be used, then Google Authenticator must also be used before a successful login. [Updated 1 Nov 2013 to add:  After a reboot, this ssh config failed with a log message of "fatal: auth2_update_methods_lists: method not in AuthenticationMethods".  Removing the ":bsdauth" made it work again (it works since the "password" authentication method will use BSD Authentication by default), but this looks like an SSH bug.]

So why might you not want to do this?  While Google Authenticator ensures that what is used over the network as a password is better than a typical user-selected password, it effectively stores a shared secret in plaintext at both ends of the connection, which is far less secure than SSH public key authentication.  If the device where Google Authenticator is present gets compromised, that secret is compromised.  And as the above link about totp-cgi points out, if you use Google Authenticator with the same secret across multiple machines, that secret is only as secure as the least secure host it's stored on, and using different secrets for different machines doesn't scale very well with the application.  A password safe with randomly generated passwords, stored in encrypted form, is probably a better solution in most cases. [Updated 2 November 2013: Authy uses the same TOTP mechanism as Google Authenticator, but encrypts the secret key on the client side.  That encryption is really more obfuscation than encryption since the key is based on phone attributes and can potentially be reverse engineered.]

As I've set it up, I'm still relying on SSH public key authentication for SSH logins, and on certificate authentication for VPN logins, in addition to Google Authenticator.  For the case of logging into my VPN from my laptop and having Google Authenticator on a separate mobile device, it does seem to be a security improvement (though I welcome anyone to show me that the gains are illusory).

UPDATE (July 31, 2019): Note that you should make the .totp-key file in the user's home directory owned by and only readable by root, or else you're effectively permitting that user to do passwordless doas/sudo, since passworded doas/sudo will use the TOTP mechanism for authentication. That won't stop the user from removing the .totp-key file and replacing it with their own, but at least that action becomes detectible. To prevent removal, on OpenBSD you can set the file to be immutable (schg flag) and run at securelevel=2. But a better solution would really be to put those secrets somewhere outside of the individual user's home directory.

UPDATE (October 22, 2019): The OpenVPN authentication with 2FA is broken in OpenBSD 6.6, it now leads to user/password authentication failures. Not sure why yet.

UPDATE (October 22, 2019 #2): Looks like it may have been user error, it works now, though I did update my _openvpnusers group to the new number (811) from the old one (596), but the number itself shouldn't be hardcoded in openvpn_bsdauth, so that shouldn't have had an impact.

UPDATE (30 October 2022): Also see Solene Rapenne's blog post on this same topic.