So far I’ve seen 2FA/MFA with OpenVPN using a 3rd Party plugin from evgeny-gridasov/openvpn-otp, but after I got it working I didn’t like the way it implemented HOTP counter storage and the use of otp-secrets. There has to be another way.

I see that there is a native, and also know that on another system we’re using the OATH toolkit for providing OTP for sshd. The OATH toolkit includes This means there is a common link for me to make use of PAM to give me MFA for OpenVPN.

Server Configuration

As I already have a configured OpenVPN server, configured with LDAP auth, all I need to install oathtool, qrencode and libpam-oath.

sudo apt install oathtool qrencode libpam-oath

Create a file for my OTP users /etc/users.oath and set it, so only nobody can read it.

touch /etc/users.oath
chown nobody: /etc/users.oath

Create an OTP secret using something that will give you a sha1 hash, eg.

openssl rand -hex 64 | sha1sum | cut -d' ' -f1

Copy the hash and then edit the /etc/users.oath file, so it includes your user’s id and the hash we copied, eg.

#type      username  pin secret-hash
HOTP/T30/6  -  6776deedd82c054d86cc87bc44324318c4334967

Create /etc/pam.d/openvpn, owned and writable only by root, readable by all. It only has one line in it.

auth requisite usersfile=/etc/users.oath window=30 digits=6

Add the plugin to the OpenVPN server.conf file.

plugin /usr/lib/openvpn/ "/etc/pam.d/openvpn login USERNAME password PASSWORD One-time OTP"

For this to work, PAM must be able to look up a user by name/id. If it can’t do this, you will get errors in the log that look like this:

client openvpn: pam_unix(openvpn:account): could not identify user (from getpwnam(

This means that you must install and configure the LDAP client for PAM. You must be able to use id in order for the PAM module to work. Even though the OpenVPN plugin for LDAP does not require PAM to succeed, the must be able to find the user.

Make sure you restart OpenVPN after these changes.

sudo systemctl restart openvpn@server

User Token Generation

To get the token generation onto a device like FreeOTP or Google Authenticator, you need to get the secret as a base32 hash. The easiest way to do it is to use oathtool to generate a token with verbose mode, eg.

$ oathtool --totp -v 6776deedd82c054d86cc87bc44324318c4334967
Hex secret: 6776deedd82c054d86cc87bc44324318c4334967
Digits: 6
Window size: 0
TOTP mode: SHA1
Step size (seconds): 30
Start time: 1970-01-01 00:00:00 UTC (0)
Current time: 2022-09-01 14:29:49 UTC (1662042589)
Counter: 0x34D5BCB (55401419)


You can then just copy the base32 secret and add it to the string as below, to generate a QR code with.

qrencode -t UTF8 'otpauth://totp/'

Using UTF8 creates a QR code on a terminal you can scan immediately. You can also use PNG and direct the output to a file you can send to the user to scan into FreeOTP, etc.

qrencode -t PNG 'otpauth://totp/' -o


PAM responds to prompts that are returned by the plugin.

The parameters are to use the openvpn pam.d file, and respond to a login prompt with the USERNAME, a password prompt (not used) with the PASSWORD, and, the final bit of magic that makes it work, a One-time prompt with the OTP.

The prompt in the debug log can be seen as:

2022-09-01 11:41:07 us=982761 PLUGIN AUTH-PAM: BACKGROUND: name match found, query/match-string ['One-time password (OATH) for `': ', 'One-time'] = 'OTP'

PAM uses the One-time as a match to the actual prompt One-time password (OATH) for '': and sends it the OTP.

This is what my server.conf ends up looking like:

proto udp
port 1194
dev tun0
topology subnet

verb 2

user nobody
group nogroup

ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/server.crt
crl-verify /etc/openvpn/crl.pem
# dh /etc/openvpn/pki/dh.pem
dh none
key /etc/openvpn/pki/private/server.key
key-direction 0
tls-auth /etc/openvpn/pki/ta.key

status /var/log/openvpn/openvpn-status.log
log /var/log/openvpn/openvpn.log

push "route"
push "route"
push "redirect-gateway def1 block-local"
push "dhcp-option DNS"
push "comp-lzo no"

plugin /usr/lib/openvpn/ /etc/openvpn/server/auth/auth-ldap.conf

auth-gen-token 43200
client-config-dir ccd
comp-lzo no
keepalive 10 60

cipher AES-256-GCM
ecdh-curve secp384r1
remote-cert-tls client
tls-cert-profile preferred
tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
tls-version-min 1.2
verify-client-cert require
plugin /usr/lib/openvpn/ "/etc/pam.d/openvpn login USERNAME password PASSWORD One-time OTP"

It’s also worth noting that this is a TOTP (Time Based One Time Password) not HOTP (HMAC based One Time Password). This means that the user.auth file is not being used to store HOTP counters.


Known issue with using --opt-verify