Real-World Problem: The Flaw in Certificate-Only VPN Access
Last year, our engineering team expanded from 12 to 45 members across three time zones. Developers, database administrators, and contractors all required round-the-clock access to staging environments and private PostgreSQL clusters on Ubuntu. Like many growing teams, we initially deployed a standard OpenVPN server. We secured it exclusively with client certificates issued through Easy-RSA.
The wake-up call came when an engineer left a company ThinkPad at a busy airport terminal. The laptop had LUKS full-disk encryption enabled, so no data leaked. Still, the incident prompted an immediate internal security audit. What we uncovered was alarming. Four developers were routinely backing up their .ovpn client files to unencrypted personal flash drives. Worse, several client keys had blank passphrases to avoid repetitive password prompts.
Certificate-only access operates on a simple assumption: whoever holds the certificate is the authorized user. If an attacker extracts an unencrypted private key from a compromised endpoint, your perimeter vanishes. No secondary prompt stops them. They gain instant access to your private subnets. We needed a setup that combined cryptographic device identity with an ephemeral, out-of-band factor.
Root Cause Analysis: Why Standard VPN Deployments Fail Under Scale
Our post-incident review revealed three structural weaknesses common to standard OpenVPN deployments:
- Static credential exposure: A unified
.ovpnprofile bundles the CA certificate, client certificate, and private key into a single plaintext file. Anyone who gains access to that file can connect immediately. - The revocation lag: Easy-RSA supports Certificate Revocation Lists (CRLs), but revoking a lost key takes time. Admins must run
./easyrsa revoke, rebuild the CRL, and publish it. In our incident, 14 hours passed between the laptop loss and the security team receiving the ticket. - Passphrase fatigue: Forcing developers to enter a 16-character key passphrase on every reconnect leads to unsafe shortcuts. Two engineers had already automated connections using shell scripts containing hardcoded passphrases.
Static cryptographic keys cannot defend a network edge against stolen hardware or leaked configuration files. Proving possession of a file is simply not enough.
Solutions Compared: Finding the Right Balance for Remote Teams
We spent four weeks evaluating three replacement architectures across a trial group of 10 engineers.
1. WireGuard with Static Public Keys
WireGuard delivered impressive performance. In our benchmarks, throughput reached 920 Mbps across a 1 Gbps link with negligible CPU utilization. However, WireGuard deliberately omits an interactive challenge-response mechanism. Adding Time-based One-Time Passwords (TOTP) requires custom port-knocking scripts or sidecar daemons that manipulate iptables dynamically. We refused to rely on fragile custom scripts for perimeter defense.
2. Commercial Zero-Trust Platforms (Tailscale, Cloudflare Zero Trust)
Cloud-managed SASE platforms make multi-factor authentication effortless through identity providers like Okta and Google Workspace. However, subscription costs range from $5 to $15 per user each month. For a team of 45 engineers and 15 contractors, that adds up to $9,000 annually. Furthermore, our SOC 2 and HIPAA compliance requirements prevented us from routing sensitive patient data pipelines through third-party control planes.
3. OpenVPN with Easy-RSA PKI and PAM Google Authenticator
OpenVPN natively integrates with Linux Pluggable Authentication Modules through the openvpn-plugin-auth-pam.so library. This architecture verifies two independent factors on every connection attempt. First, it validates the hardware profile using the Easy-RSA client certificate. Second, it checks the user’s Linux password alongside a 30-second rolling code generated by Google Authenticator.
A stolen .ovpn profile becomes completely useless on its own. An intruder still needs the engineer’s account password and their physical phone. Best of all, the entire stack runs on commodity Ubuntu compute without per-seat licensing fees.
Best Approach: Production Setup on Ubuntu 22.04 and 24.04 LTS
We deployed this architecture on an Ubuntu 22.04 LTS virtual machine (2 vCPUs, 4 GB RAM) hosting roughly 35 concurrent connections. Server load rarely exceeds 12%, and average handshake latency stays under 1.5 seconds. Follow these steps to configure the system.
Step 1: Install Required Packages
Update your apt repositories and install OpenVPN, Easy-RSA, the PAM Google Authenticator module, and iptables tools:
sudo apt update && sudo apt install -y openvpn easy-rsa libpam-google-authenticator iptables iptables-persistent
Step 2: Initialize the Easy-RSA PKI
Set up a clean directory for your Certificate Authority (CA) and generate the server-side cryptographic assets:
make-cadir ~/openvpn-ca
cd ~/openvpn-ca
./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa gen-dh
./easyrsa build-server-full server nopass
openvpn --genkey secret ta.key
Copy the generated CA certificate, Diffie-Hellman parameters, server keys, and TLS authentication key into the OpenVPN server directory:
sudo cp pki/ca.crt pki/dh.pem pki/issued/server.crt pki/private/server.key ta.key /etc/openvpn/server/
Step 3: Configure Linux PAM for 2FA
Create a dedicated PAM configuration file for OpenVPN:
sudo nano /etc/pam.d/openvpn
Add the following three directives:
auth required pam_google_authenticator.so forward_pass
auth required pam_unix.so use_first_pass
account required pam_unix.so
The forward_pass argument enables single-string entry. When a user submits their password and a six-digit TOTP code together, PAM extracts the token for verification and hands the raw password to pam_unix.so.
Step 4: Configure the OpenVPN Server Daemon
Save the primary daemon configuration file at /etc/openvpn/server/server.conf:
port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
tls-auth ta.key 0
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist /var/log/openvpn/ipp.txt
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
keepalive 10 120
cipher AES-256-GCM
auth SHA256
user nobody
group nogroup
persist-key
persist-tun
status /var/log/openvpn/openvpn-status.log
verb 3
explicit-exit-notify 1
# Dual-Factor PAM Integration
plugin /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn
verify-client-cert require
username-as-common-name
reneg-sec 0
The username-as-common-name directive enforces a critical security invariant: the login username must match the Common Name embedded in the client certificate. Setting reneg-sec 0 disables hourly key renegotiations so active sessions are not dropped mid-work.
Step 5: Enable IP Forwarding and NAT Masquerading
Enable IPv4 packet routing in sysctl:
sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
sudo sysctl -p
Find your public network interface using ip route show default (typically eth0 or ens3). Apply the NAT routing rule and launch the OpenVPN service:
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
sudo netfilter-persistent save
sudo systemctl enable --now openvpn-server@server
Step 6: Provision User Accounts and TOTP Keys
For each engineer, create a system user without interactive shell access:
sudo useradd -m -s /usr/sbin/nologin devuser1
sudo passwd devuser1
Initialize the Google Authenticator profile under their account:
sudo -u devuser1 google-authenticator -t -d -f -r 3 -R 30 -W
This command configures time-based tokens, prevents token reuse, rate-limits brute force attempts to 3 requests per 30 seconds, and writes the seed file to ~/.google_authenticator. Display the resulting QR code to the engineer so they can scan it into their authenticator app. Store the 5 emergency scratch codes in a company password manager.
Step 7: Build the Client Configuration Profile
Generate a dedicated client certificate whose Common Name matches the system username exactly:
cd ~/openvpn-ca
./easyrsa build-client-full devuser1 nopass
Assemble the client file (devuser1.ovpn) with inline cryptographic blocks:
client
dev tun
proto udp
remote 203.0.113.10 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
key-direction 1
# Trigger interactive credential prompts
auth-user-pass
reneg-sec 0
<ca>
# Paste /etc/openvpn/server/ca.crt
</ca>
<cert>
# Paste ~/openvpn-ca/pki/issued/devuser1.crt
</cert>
<key>
# Paste ~/openvpn-ca/pki/private/devuser1.key
</key>
<tls-auth>
# Paste /etc/openvpn/server/ta.key
</tls-auth>
Step 8: Day-to-Day Operations and Client Authentication
When connecting through OpenVPN GUI, Tunnelblick, or the command line, users receive standard login prompts. In the username box, the user enters devuser1. In the password box, they enter their Unix account password concatenated directly with their 6-digit TOTP code (for example: SecretPassphrase839201).
Over the past six months, this configuration handled more than 4,200 unique sessions without a single unexplained disconnect. Misplaced hardware is no longer an emergency. Even if an attacker extracts an active .ovpn profile, your private network remains completely inaccessible without the physical authenticator device.

