← Start of tutorial

WIUNIX ACADEMY

OpenVPN MFA with client certificates and Google Authenticator

Page 3 of 5. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Debian 12Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment7 editable values
10

LET’S BUILD

Create the pilot server configuration

Save the following complete file. Do not merge it into the production listener. Startup verifies the key/certificate match; a failed start must remain stopped while investigated. Current AEAD data ciphers and TLS1.2 minimum are used, with no compression or obsolete BF-CBC/cipher fallback. DCO is disabled in this small conventional-TUN pilot for consistent diagnostics.

bash

Dedicated pilot VM; private terminal for enrollment

set -eu
sudo sh -c 'umask 077; set -C; : > "/srv/academy-openvpn-mfa/server.conf"'
sudo nano '/srv/academy-openvpn-mfa/server.conf'
How do I know it worked?

A new private pilot configuration is ready to edit.

11

LET’S BUILD

Require both verifiers and retain certificate names

The one-hour renegotiation setting is not a promised session lifetime or a token bypass. Test fresh prompts across renegotiation, reconnect and suspend with the actual client. There is no management listener, credential logging, optional auth or duplicate-CN mode. PAM state writes happen through its privileged helper after the main daemon drops to nobody/nogroup.

text · /srv/academy-openvpn-mfa/server.conf

Dedicated pilot VM; private terminal for enrollment

local 192.0.2.40
port 11940
proto udp4
dev tun-mfa
dev-type tun
topology subnet
server 10.93.0.0 255.255.255.0
ca /srv/academy-openvpn-mfa/pki/ca.crt
cert /srv/academy-openvpn-mfa/pki/server.crt
key /srv/academy-openvpn-mfa/pki/server.key
dh none
crl-verify /srv/academy-openvpn-mfa/pki/crl.pem
tls-crypt /srv/academy-openvpn-mfa/pki/tls-crypt.key
verify-client-cert require
remote-cert-tls client
tls-version-min 1.2
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
allow-compression no
disable-dco
plugin /srv/academy-openvpn-mfa/auth-pam.so "academy-mfa login USERNAME Verification PASSWORD"
script-security 2
auth-user-pass-verify /srv/academy-openvpn-mfa/bind-cn.py via-file
tmp-dir /srv/academy-openvpn-mfa/auth-tmp
user nobody
group nogroup
persist-key
persist-tun
keepalive 10 60
reneg-sec 3600
verb 3
How do I know it worked?

The complete candidate requires verified client certificates, OTP PAM and exact CN binding.

12

LET’S BUILD

Prepare a dedicated systemd unit

The unit is deliberately separate from distribution auto-discovery directories and existing VPN units. It starts as root so the packaged PAM helper can initialize; the OpenVPN configuration then drops the main daemon. Do not add systemd User=nobody, NoNewPrivileges or filesystem restrictions without testing their effect on PAM identity switching and state writes. The scoped unit is not a complete hardening profile.

bash

Dedicated pilot VM; private terminal for enrollment

set -eu
sudo sh -c 'umask 022; set -C; : > "/etc/systemd/system/academy-mfa.service"'
sudo nano '/etc/systemd/system/academy-mfa.service'
How do I know it worked?

A fresh isolated unit file is ready; no existing unit is overwritten.

13

LET’S BUILD

Run only the reviewed pilot

The config is root-readable; OpenVPN reads it and private key before dropping privileges. The seed remains per-user0600. Do not enable boot start until both positive and negative acceptance checks pass and the owner of this new VM approves operational ownership.

ini · /etc/systemd/system/academy-mfa.service

Dedicated pilot VM; private terminal for enrollment

[Unit]
Description=WIUNIX Academy certificate and TOTP pilot
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/sbin/openvpn --config /srv/academy-openvpn-mfa/server.conf
Restart=on-failure
RestartSec=5
UMask=0077

[Install]
WantedBy=multi-user.target
How do I know it worked?

The unit invokes only this dedicated configuration; automatic boot start remains off.

14

LET’S BUILD

Validate the unit and start with the console open

Run this on the dedicated VM with the independent console still open. OpenVPN has no general full-config dry-run flag; systemd-analyze checks unit syntax, not TLS/auth correctness. A real bounded startup is the configuration check here. Read the journal privately and stop for any plugin, CRL, key, TUN or permission failure. Never add insecure options to make startup succeed.

bash

Dedicated pilot VM; private terminal for enrollment

set -eu
sudo systemd-analyze verify '/etc/systemd/system/academy-mfa.service'
sudo systemctl daemon-reload
sudo systemctl start 'academy-mfa.service'
sudo systemctl is-active 'academy-mfa.service'
sudo journalctl -u 'academy-mfa.service' -n 60 --no-pager
ip -br address show dev 'tun-mfa'
sudo ss -lunp
ps -eo user,pid,ppid,args | grep '[o]penvpn'
How do I know it worked?

The pilot is active on UDP 11940, tun-mfa has10.93.0.1/24 and the main daemon runs as nobody; the PAM helper remains privileged.

15

LET’S BUILD

Prepare a private interactive client profile

On a trusted Linux pilot client, create a new0700 directory through your normal private-file workflow, and place the client CA/certificate/key and tls-crypt key there using protected transfer. Save the next file as pilot.ovpn beside them; never paste private keys into this article or reader settings. The pilot certificate CN is academyvpn. Protect the key with your endpoint policy and preferably a passphrase supported by the client.

The profile validates both serverAuth purpose and the exact server certificate name vpn-server, not only the issuing CA. The hostname vpn.example.net is the transport destination; this certificate identity is intentionally fixed by the PKI prerequisite. Other GUI platforms need an explicitly tested import/prompt/key-storage workflow.

bash

Trusted Linux pilot client

umask 077
test ! -e pilot.ovpn && test ! -L pilot.ovpn
set -C
: > pilot.ovpn
set +C
nano pilot.ovpn
How do I know it worked?

A new private profile in the client’s dedicated directory is ready, with private credentials transferred separately.

16

LET’S BUILD

Keep server validation and fresh OTP prompts

Do not append an auth-user-pass filename; that would turn a fresh prompt into stored code reuse. No inline private-key example is included. auth-nocache clears cached input but does not guarantee that every GUI client prompts correctly; test the supported versions.

text · pilot.ovpn

Trusted Linux pilot client

client
dev tun
proto udp4
remote vpn.example.net 11940
nobind
resolv-retry infinite
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
tls-crypt tls-crypt.key
remote-cert-tls server
verify-x509-name vpn-server name
tls-version-min 1.2
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
allow-compression no
disable-dco
auth-user-pass
auth-nocache
auth-retry interact
verb 3
How do I know it worked?

The client verifies vpn-server and asks for username and a fresh code interactively.

Explore all tutorials ↗