OpenVPN Server on Ubuntu with Google Authenticator 2FA and Easy-RSA PKI

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

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 .ovpn profile 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.

Share: