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).
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.
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.
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:
sudo apt install openvpn
step.openvpn-server@server
(not openvpn@server
)../sample/sample-config-files/server.conf
in the OpenVPN repository.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.
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
.
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!
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!
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