openvpn & google authenticator totp
July 03, 2020
7 min read

credit where credit is due

This post is largley inspired by the pains I went through in setting up an OpenVPN server that supports MFA using Google Authenticator-based TOTP. This forum post gave me a huge nudge in the right direction for finalizing my setup.

Also, this medium post from Egon Braun is a great guide for setting up Google Authenticator token support on your server.

The below sections will guide you through setting up an OpenVPN server with support for Google Authenticator TOTP-based Multi-Factor Authentication (MFA).

client support

In general, I’d recommed taking a look at the “Challenge/Response Protocol” section of the OpenVPN management-notes.txt for more a better understanding of how this all works.

OpenVPN supports prompt-based MFA using the static-challenge option in a client configuration file.

For example, to ask a user for a TOTP code after they have already authenticated with normal credentials (e.g. username/password or certificates), you would need something like this in a client configuration file:

...

# this will prompt a user for a TOTP code
# the "1" means "don't password-mask the TOTP code"
static-challenge "Enter Google Authenticator Code:" 1

...

This works great for clients but it unfortunately is not supported on OpenVPN servers at versions 2.4.9 and below.

At the time of writing OpenVPN clients known to support this option are:

Additionally, generating client configurations can be kind of a pain and typically you’ll want to write a script to do it. As an example, I’ve provided an client config generator below. This is just an example, you probably shouldn’t use it as a permanent solution for your use case.

Also, an example of a base client configuration file can be found here.

server setup

While OpenVPN clients support the static-challenge option, current releases of OpenVPN server do not support handling the provided code for basic PAM plugin modules. Specifically, OpenVPN needs to be able to accept a specific PAM plugin argument that includes the string pin OTP. The current release (2.4.9) does not support this option.

Fortunately, the master branch of OpenVPN (2.5) does support this option, and it is relatively easy to build.

build & install openvpn

Install dependencies for building OpenVPN from source:

$ sudo apt-get install build-essential autoconf libtool pkg-config liblzo2-dev libssl-dev libsystemd-dev libpam-dev

Clone the OpenVPN repo:

$ git clone https://github.com/OpenVPN/openvpn.git

Then build the OpenVPN server and install it:

$ autoreconf -i -v -f
$ ./configure --enable-systemd
$ make
$ sudo make install

At this point OpenVPN will be runnable, but without a configuration file there won’t really be anything to run. I recommend following the DigitalOcean tutorials for Ubuntu16.04 or Ubuntu18.04 for getting setup with a base configuration for your server.

Some notes about the above guides:

  • Since we built from source, you will need to omit the sudo apt install openvpn step.
  • The systemd service with a source build will be openvpn-server@server (not openvpn@server).
  • The referenced sample server config can be found at ./sample/sample-config-files/server.conf in the OpenVPN repository.

configure server for totp

After you’ve got a base configuration ready, you need to tell OpenVPN to use PAM authentication routines and to include an MFA challenge/response (note pin OTP in the argument string).

Update /etc/openvn/server.conf with:

# "... pin OTP" tells OpenVPN to include a static-challenge pin when invoking PAM modules
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so "openvpn login USERNAME password PASSWORD pin OTP"

The location of openvpn-plugin-auth-pam.so may vary system by system. Just use find / | grep openvpn-plugin-auth-pam to find it.

setup google authenticator

Now you can install the Google Auth PAM module and create a token for your user.

Install it from apt:

$ sudo apt-get install -y libpam-google-authenticator

First, setup the gauth directories and a user for reading:

$ sudo addgroup gauth
$ sudo useradd -g gauth gauth
$ sudo mkdir /etc/openvpn/google-authenticator
$ sudo chown gauth:gauth /etc/openvpn/google-authenticator
$ sudo chmod 0700 /etc/openvpn/google-authenticator

Then make a google-authenticator token for your user (adapted from this post):

$ sudo su -c "google-authenticator -t -d -r3 -R30 -f -l \"My VPN\" -s /etc/openvpn/google-authenticator/test-user" - gauth
Warning: pasting the following URL into your browser exposes the OTP secret to Google:
  <mfa url>

<ascii qr code>

Your new secret key is: <totp-key>
...
By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew ...
Do you want to do so? (y/n) n

At this point you can use the Google Authenticator mobile app to scan the code, then it will be usable for authentication later.

Alternatively, you can generate a PNG file using qrencode, e.g.:

$ sudo apt-get install -y qrencode
$ qrencode -o test-user.png otpauth://totp/label?secret=some-secret-key

The simplest version of URI format needed by qrencode is: otpauth://totp/<uri-quoted-label>?secret=<totp-secret>.

For example, with label “My VPN” and TOTP secret “superseekrit”, your qrencode URI would be: otpauth://totp/My%20VPN?secret=superseekrit.

configure pam

Last but not least, you need to configure the PAM module we told OpenVPN to invoke when handling authentication from clients.

Specifically, you should make sure authtok_prompt=pin is in the argument list (the secret= option is based on the “setup google authenticator” section above).

Update/add an /etc/pam.d/openvpn file with the following:

...

# authtok_prompt=pin tells the pam module to look for an OTP code following the string "pin".
auth required pam_google_authenticator.so authtok_prompt=pin secret=/etc/openvpn/google-authenticator/${USER} user=gauth

...

Then run sudo systemctl restart openvpn-server@server and you will be ready to accept TOTP-based authentication!

client configuration

Now that your server has been configured to accept Google Authenticator TOTP codes, you will want to make sure client configurations have something similar to the following:

static-challenge "Enter Google Authenticator Code:" 1

This will instruct client programs to open a dialog prompt to ask for a TOTP code before completing the OpenVPN login dance.

Go forth and enjoy OpenVPN + MFA/TOTP!

example client config generator

NOTE: You should NOT use this for any production use cases. You will likely have your own tweaks you need to make, and you probably don’t want the CA and OpenVPN server on the same machine. This is for testing purposes only.

Save this as something like make-conf.sh and invoke it like ./make-conf.sh my-username:

#!/bin/bash

# Bail on errors
set -e

# config dirs
SERVER_CONFIG_DIR="/etc/openvpn"
CLIENT_CONFIG_DIR="${HOME}/clients"

# ca cert and tls-auth key
CA_CERT=$(sudo cat ${SERVER_CONFIG_DIR}/ca.crt)
TA_KEY=$(sudo cat ${SERVER_CONFIG_DIR}/ta.key)

# Expect a user id as a single arg.
USER_ID="${1:?user id is required}"

OUTPUT_DIR="${CLIENT_CONFIG_DIR}/${USER_ID}"
if [ ! -d "${OUTPUT_DIR}" ]; then
    mkdir -p "${OUTPUT_DIR}"
fi

## PKI stuff

# These could be anywhere
CA_CRYPT="${HOME}/ca/EasyRSA-3.0.4"
SERVER_CRYPT="${HOME}/server/EasyRSA-3.0.4"

# Generate a client key
CLIENT_REQ_FILE="${SERVER_CRYPT}/pki/reqs/${USER_ID}.req"
CLIENT_KEY_FILE="${SERVER_CRYPT}/pki/private/${USER_ID}.key"
CLIENT_CA_REQ_FILE="${CA_CRYPT}/pki/reqs/${USER_ID}.req"
CLIENT_CERT_FILE="${CA_CRYPT}/pki/issued/${USER_ID}.crt"
if [ -f "${CLIENT_CA_REQ_FILE}" ]; then
  rm "${CLIENT_CA_REQ_FILE}"
fi
if [ -f "${CLIENT_KEY_FILE}" ]; then
  rm "${CLIENT_KEY_FILE}"
fi
if [ -f "${CLIENT_REQ_FILE}" ]; then
  rm "${CLIENT_REQ_FILE}"
fi

# Generate a cert request for the client.
pushd ${SERVER_CRYPT}
  printf "%s\n" ${USER_ID} | ./easyrsa gen-req ${USER_ID} nopass
popd

# Import and sign the request using a CA
pushd ${CA_CRYPT}
 ./easyrsa import-req "${CLIENT_REQ_FILE}" "${USER_ID}"
 printf "yes\n" |./easyrsa sign-req client ${USER_ID}
popd

# Dump it all to a `.ovpn` configuration file.
CLIENT_CERT=$(openssl x509 -in ${CLIENT_CERT_FILE} -outform pem)
CLIENT_KEY=$(cat ${CLIENT_KEY_FILE})

# This could be anywhere and have whatever you want in it.
BASE_CONFIG=$(cat ${CLIENT_CONFIG_DIR}/base.conf)

# Generate the config file.
cat <<EOF > "${OUTPUT_DIR}/vpn.ovpn"

## Base configuration

${BASE_CONFIG}

## BEGIN PKI

<ca>
${CA_CERT}
</ca>
<cert>
${CLIENT_CERT}
</cert>
<key>
${CLIENT_KEY}
</key>
<tls-auth>
${TA_KEY}
</tls-auth>

## END PKI
EOF