Here is a quick guide and an authentication script to support using a Yubikey OTP for two-factor authentication on an OPNsense OpenVPN server. This approach works alongside username/password authentication, including when using different OPNsense authentication non-local backends such as LDAP and Radius.
We put this authentication script together as a proof-of-concept after having multiple folks ask us for help. For a standard OpenVPN setup, such as on Ubuntu, you can follow our guide here instead.
It has the advantage that it doesn’t replace or bypass OPNsense’s authentication process, so authentication you’ve configured in OPNsense will still apply. However please note that as it’s a proof-of-concept, we don’t recommend using it as is for large deployments as-is (mostly due to it using a simple local database file), you should review it to be sure it suits your security model, and future OPNsense releases may break it.
The following steps assume you already have an OpenVPN server setup in OPNsense with username/password authentication. These steps have been tested on the latest version of OPNsense at the time of writing (25.7.2), and assume you have SSH/SFTP and web admin access to the OPNsense server.
- Log into the admin web interface for OpenVPN, go to VPN → OpenVPN → Servers, and click the Edit button next to your existing server.
- In your web browser’s address bar take note of the ID of the server. For example, the address bar should have something like
https://192.168.0.1/vpn_openvpn_server.php?act=edit&id=0. In this example we can seeid=0, so the ID is “0”. We’ll need this number later. - Scroll down to Advanced and add the following lines. These set a custom authentication script to use, set a security level that allows the script to run, and also ensures that session tokens are used (to avoid needing to prompt the user for their Yubikey OTP every time a renegotiation takes place):
auth-user-pass-verify /scripts/opnsense_openvpn_auth.py via-env script-security 3 auth-gen-token 86400 - Save the changes.
- If you don’t already have a Yubico API Client ID and Secret Key, visit Yubico API key signup and create one. You’ll need these details later.
- SSH into the OPNsense server, and select the Shell option.
- Enter the command
mkdir /scriptsto create a directory. - On your computer, create a plain text file called
opnsense_openvpn_auth.pyand enter the following source code:#!/usr/local/bin/python3 import sys, os from base64 import b64decode # Replace the following details: serverID = "1" yubicoClientId = "" yubicoSecretKey = "" # Read in the username and password from the environment username = os.environ['username'] password = os.environ['password'] # Extract the token from the encoded password if password.startswith('SCRV1:'): # Extract the actual password and token passwordSplit = password.split(':') password = b64decode(passwordSplit[1]).decode("utf-8") token = b64decode(passwordSplit[2]).decode("utf-8") else: # Invalid data sys.exit(1) # Check the username and password using pfSense's authentication script. This # supports PAM, LDAP, Radius, etc. os.environ['password'] = password import subprocess try: subprocess.check_call(["/usr/local/opnsense/scripts/openvpn/ovpn_event.py", serverID]) loginValid = True except: loginValid = False if not loginValid: sys.exit(1) # The first 12 characters of the token is the unique public ID tokenId = token[:12] # Only accept the token assigned to the user. If this is the first time # a token has been used for a user, assign it to the user. import pickle dbPath = '/scripts/token_index.db' if os.path.exists(dbPath): try: file = open(dbPath,'rb') tokenDb = pickle.load(file) file.close() except: tokenDb = {} else: tokenDb = {} updateDb = False if username in tokenDb.keys() and tokenDb[username] != tokenId: # The token being used does not match the token for the user sys.exit(1) else: tokenDb[username] = tokenId updateDb = True # Check that the token is valid using the Yubico Cloud service from yubico_client import Yubico yubico = Yubico(yubicoClientId, yubicoSecretKey) try: tokenValid = yubico.verify(token) except: tokenValid = False if not tokenValid: sys.exit(1) # Save the token database if necessary if updateDb: file = open(dbPath,'wb') pickle.dump(tokenDb, file, -1) file.close() sys.exit(0) - Edit the script and set the
serverID,yubicoClientId, andyubicoSecretKeyvariables to those we noted earlier. - Upload the opnsense_openvpn_auth.py file to the /scripts directory we created on the OPNsense server using SFTP (such as using the command line or using a SFTP client like Transmit or Cyberduck).
- Download the Python yubico-client source distribution from yubico-client · PyPI and extract it on your computer.
- Inside the extracted folder there should be a
yubico_clientdirectory. Upload this directory into the /scripts directory using SFTP. - In Viscosity, edit your VPN connection for the OPNsense server (or import a new one), and under the Advanced tab add the following advanced command on a new line and then click Save. Feel free to customise the message that appears.
static-challenge "Enter code:" 0 - Try connecting to your OpenVPN server.
When connecting users will first be prompted for their username/password as normal, and then be prompted for their Yubikey OTP (you can customise the message that appears).
The first time a user successfully connects and authenticates, the Yubikey used will become associated with their username. If they attempt to use a different Yubikey then authentication will fail.
Cheers,
James