A battle-tested, SSL/TLS-based VPN — providing encrypted site-to-site and remote-access tunnels over UDP or TCP, with a mature PKI, flexible routing, and clients for every platform.
PKI · Easy-RSA · tun/tap · UDP/TCP · full-tunnel · split-tunnel · Linux · Windows · macOS · iOS · Android
01 — How OpenVPN Works
OpenVPN uses TLS for the control channel (authentication, key exchange) and a fast symmetric cipher for the data channel. It creates a virtual network interface (tun0 for routed IP traffic, tap0 for bridged Ethernet) and routes packets through the encrypted tunnel between client and server.
OpenVPN's strength is flexibility — it runs over UDP or TCP, on any port, through almost any firewall, and its PKI model scales from a single home server to thousands of concurrent corporate clients.
| Concept | Meaning |
| PKI | Public Key Infrastructure — the CA, server cert, and per-client certs that authenticate both ends of the tunnel. |
| CA | Certificate Authority — the root of trust. Signs all server and client certificates. |
| tun | Layer-3 routed mode. Carries IP packets. Use for most VPN setups. |
| tap | Layer-2 bridged mode. Carries Ethernet frames. Use when you need broadcast or non-IP protocols. |
| Easy-RSA | The PKI toolkit bundled with OpenVPN for creating and managing CAs and certificates. |
| tls-crypt | A pre-shared key that authenticates and encrypts the TLS handshake itself — stops port scanners from even identifying the server as OpenVPN. |
| Full tunnel | All client traffic (including internet) routes through the VPN. |
| Split tunnel | Only traffic to specific subnets routes through the VPN. Everything else uses the client's normal gateway. |
CLIENT VPN SERVER PRIVATE NETWORK
┌───────────────┐ ┌──────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ tun0 │◄─────────────│ tun0 │ │ 10.8.0.0/24 │
│ 10.8.0.2 │ encrypted │ 10.8.0.1 │────────────►│ 192.168.1.x │
│ │ UDP/TCP │ │ routing │ │
└───────────────┘ └──────────────────┘ └──────────────┘
192.168.x.x (LAN) 203.0.113.5 (public IP)
02 — Installation
Debian / Ubuntu — server and client
sudo apt update && sudo apt install openvpn easy-rsa
# Verify
openvpn --version
RHEL / Fedora / Rocky / AlmaLinux
sudo dnf install epel-release
sudo dnf install openvpn easy-rsa
openvpn --version
Arch Linux
sudo pacman -S openvpn easy-rsa
Windows client
# Download the official installer from: https://openvpn.net/community-downloads/
# Run as Administrator — installs the TAP/TUN adapter and OpenVPN GUI
# Import a .ovpn profile via the system tray icon → Import file
macOS client
# Tunnelblick (free, open source)
# https://tunnelblick.net
# Double-click a .ovpn file to import it
# Or via Homebrew
brew install --cask tunnelblick
iOS / Android
# Install "OpenVPN Connect" from the App Store or Google Play
# Import the .ovpn profile file via AirDrop, email, or QR code
03 — Building the PKI with Easy-RSA
Every OpenVPN deployment needs a PKI — a CA that signs a server certificate and one certificate per client. Easy-RSA makes this manageable from the command line. Do all PKI work on a secure machine, ideally not the VPN server itself.
Initialise the PKI directory
# Copy Easy-RSA to a working directory
make-cadir ~/openvpn-ca
cd ~/openvpn-ca
# Initialise a fresh PKI
./easyrsa init-pki
Edit vars (optional but recommended)
# ~/openvpn-ca/vars — set your organisation details
set_var EASYRSA_REQ_COUNTRY "PT"
set_var EASYRSA_REQ_PROVINCE "Porto"
set_var EASYRSA_REQ_CITY "Porto"
set_var EASYRSA_REQ_ORG "MyOrg"
set_var EASYRSA_REQ_EMAIL "admin@example.com"
set_var EASYRSA_REQ_OU "VPN"
set_var EASYRSA_KEY_SIZE 4096 # RSA key size
set_var EASYRSA_ALGO ec # or "rsa"
set_var EASYRSA_CURVE secp384r1 # EC curve
set_var EASYRSA_CA_EXPIRE 3650 # CA validity: 10 years
set_var EASYRSA_CERT_EXPIRE 825 # cert validity: ~2 years
Build the CA
# Build the Certificate Authority — set a strong passphrase when prompted
./easyrsa build-ca
# Files created:
# pki/ca.crt — CA certificate (distribute to all clients)
# pki/private/ca.key — CA private key (NEVER share — keep offline)
Generate the server certificate
# Generate server key + CSR, then sign it with the CA
./easyrsa gen-req server nopass
./easyrsa sign-req server server
# Files created:
# pki/issued/server.crt
# pki/private/server.key
Generate Diffie-Hellman parameters
# DH params for key exchange (only needed if not using ECDH)
./easyrsa gen-dh
# File created: pki/dh.pem
# This takes a few minutes — grab a coffee
Generate the tls-crypt key
# A pre-shared key for authenticating the TLS handshake
# Prevents DoS attacks and hides the server from port scanners
openvpn --genkey secret ~/openvpn-ca/pki/ta.key
Generate a client certificate
# Replace "client1" with the actual client name — use one cert per user/device
./easyrsa gen-req client1 nopass
./easyrsa sign-req client client1
# Files created:
# pki/issued/client1.crt
# pki/private/client1.key
# Repeat for each additional client:
./easyrsa gen-req client2 nopass
./easyrsa sign-req client client2
Revoke a client certificate
# Revoke a client (e.g. lost device, terminated employee)
./easyrsa revoke client1
# Regenerate the Certificate Revocation List
./easyrsa gen-crl
# Copy the updated CRL to the server and reload OpenVPN
sudo cp pki/crl.pem /etc/openvpn/server/
sudo systemctl reload openvpn-server@server
⚠ Keep pki/private/ca.key offline and backed up securely. If your CA key is compromised, every certificate it signed must be replaced. The CA key should never touch the VPN server itself.
04 — Server Setup
Copy PKI files to the server
sudo mkdir -p /etc/openvpn/server
# From your PKI machine, copy to the VPN server:
sudo cp pki/ca.crt /etc/openvpn/server/
sudo cp pki/issued/server.crt /etc/openvpn/server/
sudo cp pki/private/server.key /etc/openvpn/server/
sudo cp pki/dh.pem /etc/openvpn/server/ # if using RSA
sudo cp pki/ta.key /etc/openvpn/server/
sudo cp pki/crl.pem /etc/openvpn/server/
# Lock down permissions
sudo chmod 600 /etc/openvpn/server/server.key
sudo chmod 600 /etc/openvpn/server/ta.key
/etc/openvpn/server/server.conf — full annotated config
# ── Network ────────────────────────────────────────────────────────────
# Listen on UDP 1194 (change port to avoid filters; use TCP 443 to bypass strict firewalls)
port 1194
proto udp # udp is faster; use tcp if UDP is blocked
dev tun # layer-3 routed tunnel
# ── PKI ────────────────────────────────────────────────────────────────
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/server.crt
key /etc/openvpn/server/server.key
dh /etc/openvpn/server/dh.pem # omit if using ECDH (ecdh-curve directive)
crl-verify /etc/openvpn/server/crl.pem
# TLS authentication key — must match direction on client (server=0, client=1)
tls-crypt /etc/openvpn/server/ta.key
# ── Tunnel addressing ──────────────────────────────────────────────────
# VPN subnet — clients get IPs from this range
server 10.8.0.0 255.255.255.0
# Persist IP assignments across reconnects
ifconfig-pool-persist /var/log/openvpn/ipp.txt
# ── Routing ────────────────────────────────────────────────────────────
# OPTION A — Full tunnel: route ALL client traffic through the VPN
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
# OPTION B — Split tunnel: only route specific subnets through the VPN
# (comment out the redirect-gateway line above, uncomment these)
# push "route 192.168.1.0 255.255.255.0"
# push "route 10.0.0.0 255.255.0.0"
# Allow clients to reach each other through the VPN
client-to-client
# ── Keepalive & reliability ────────────────────────────────────────────
keepalive 10 120 # ping every 10s, assume dead after 120s
persist-key
persist-tun
# ── Security ───────────────────────────────────────────────────────────
cipher AES-256-GCM # modern AEAD cipher
auth SHA256 # HMAC digest
tls-version-min 1.2 # refuse TLS < 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384
# Drop privileges after startup
user nobody
group nogroup # use "nobody" on RHEL/Fedora
# ── Logging ────────────────────────────────────────────────────────────
status /var/log/openvpn/openvpn-status.log
log-append /var/log/openvpn/openvpn.log
verb 3 # verbosity: 0=silent, 3=normal, 6=debug
mute 20 # suppress repeated log lines
# ── Misc ───────────────────────────────────────────────────────────────
max-clients 100 # maximum simultaneous connections
compress lz4-v2 # compression (disable if all traffic is already encrypted)
push "compress lz4-v2"
Enable IP forwarding and NAT
# Enable IP forwarding permanently
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-openvpn.conf
sudo sysctl -p /etc/sysctl.d/99-openvpn.conf
# NAT — masquerade VPN client traffic through the server's public interface
# Replace eth0 with your actual internet-facing interface
# iptables
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i tun0 -j ACCEPT
sudo iptables -A FORWARD -o tun0 -j ACCEPT
# Make iptables rules persistent
sudo netfilter-persistent save
# nftables equivalent
sudo nft add table ip nat
sudo nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; }'
sudo nft add rule ip nat postrouting ip saddr 10.8.0.0/24 oifname "eth0" masquerade
Firewall — open the OpenVPN port
# ufw
sudo ufw allow 1194/udp
sudo ufw reload
# firewalld
sudo firewall-cmd --zone=public --add-service=openvpn --permanent
sudo firewall-cmd --reload
# iptables
sudo iptables -A INPUT -p udp --dport 1194 -j ACCEPT
Create log directory and start the server
sudo mkdir -p /var/log/openvpn
# Start and enable the server
sudo systemctl enable --now openvpn-server@server
# Check status
sudo systemctl status openvpn-server@server
# Follow the log
sudo tail -f /var/log/openvpn/openvpn.log
✔ The @server suffix in the systemd unit name corresponds to the config filename — /etc/openvpn/server/server.conf. If you named your config myvpn.conf, the unit would be openvpn-server@myvpn.
05 — Per-Client Configuration Files
Each client needs a .ovpn profile — a single portable file that bundles the connection config, CA certificate, client certificate, client key, and TLS key together. Generate one per client, distribute it securely, and keep it secret.
client1.ovpn — inline certificate format (recommended)
client
dev tun
proto udp
remote YOUR_SERVER_IP_OR_DOMAIN 1194 # ← replace with your server's public IP
resolv-retry infinite
nobind
persist-key
persist-tun
# Security — must match server
cipher AES-256-GCM
auth SHA256
tls-version-min 1.2
# TLS key direction (client must use direction 1, server uses 0)
key-direction 1
verb 3
# ── Inline certificates ─────────────────────────────────────────────
# Paste the contents of each file between the tags below
<ca>
-----BEGIN CERTIFICATE-----
(paste contents of ca.crt here)
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
(paste contents of client1.crt here)
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
(paste contents of client1.key here)
-----END PRIVATE KEY-----
</key>
<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
(paste contents of ta.key here)
-----END OpenVPN Static key V1-----
</tls-crypt>
Script — auto-generate a client .ovpn file
#!/bin/bash
# gen-client.sh — run from ~/openvpn-ca
# Usage: ./gen-client.sh client1
CLIENT=$1
PKI=~/openvpn-ca/pki
SERVER_IP="YOUR_SERVER_IP"
SERVER_PORT="1194"
PROTO="udp"
OUTPUT=~/${CLIENT}.ovpn
cat > "$OUTPUT" <<EOF
client
dev tun
proto ${PROTO}
remote ${SERVER_IP} ${SERVER_PORT}
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-GCM
auth SHA256
tls-version-min 1.2
key-direction 1
verb 3
EOF
echo "<ca>" >> "$OUTPUT"
cat "$PKI/ca.crt" >> "$OUTPUT"
echo "</ca>" >> "$OUTPUT"
echo "<cert>" >> "$OUTPUT"
# Extract only the certificate block
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' \
"$PKI/issued/${CLIENT}.crt" >> "$OUTPUT"
echo "</cert>" >> "$OUTPUT"
echo "<key>" >> "$OUTPUT"
cat "$PKI/private/${CLIENT}.key" >> "$OUTPUT"
echo "</key>" >> "$OUTPUT"
echo "<tls-crypt>" >> "$OUTPUT"
cat "$PKI/ta.key" >> "$OUTPUT"
echo "</tls-crypt>" >> "$OUTPUT"
chmod 600 "$OUTPUT"
echo "Profile written to $OUTPUT"
# Generate profiles for each client
chmod +x gen-client.sh
./gen-client.sh client1
./gen-client.sh client2
⚠ The .ovpn file contains the client's private key — treat it like a password. Transfer it only over encrypted channels (SCP, SFTP, Signal). Never send it by email or store it in a public location.
06 — Client Setup — Linux
Install OpenVPN client
# Debian / Ubuntu
sudo apt install openvpn
# RHEL / Fedora
sudo dnf install openvpn
Connect from the command line
# Connect using the .ovpn profile (runs in foreground)
sudo openvpn --config client1.ovpn
# Connect in the background
sudo openvpn --config client1.ovpn --daemon
# With verbose logging to see what's happening
sudo openvpn --config client1.ovpn --verb 6
Connect via systemd (persistent, auto-start)
# Place the config in the OpenVPN client directory
sudo cp client1.ovpn /etc/openvpn/client/client1.conf
# Enable and start
sudo systemctl enable --now openvpn-client@client1
# Check status
sudo systemctl status openvpn-client@client1
# Follow the log
sudo journalctl -u openvpn-client@client1 -f
Verify the tunnel is working
# A tun0 interface should appear
ip addr show tun0
# The VPN server's gateway should be in your routing table
ip route show
# Ping the VPN server's tunnel IP
ping 10.8.0.1
# Full-tunnel: check your public IP has changed
curl https://ifconfig.me
# Split-tunnel: verify VPN routes are present
ip route | grep 10.8
Disconnect
# If running in foreground — Ctrl+C
# If running as a daemon
sudo pkill openvpn
# If running as a systemd service
sudo systemctl stop openvpn-client@client1
07 — Client Setup — Windows
Install and import
# 1. Download OpenVPN GUI from https://openvpn.net/community-downloads/
# 2. Run the installer as Administrator
# 3. Copy client1.ovpn to:
# C:\Users\YourName\OpenVPN\config\client1.ovpn
# (the GUI also accepts drag-and-drop import)
# 4. Right-click the OpenVPN system tray icon → Connect
Command line (OpenVPN 2.5+)
# Run PowerShell as Administrator
cd "C:\Program Files\OpenVPN\bin"
# Connect
.\openvpn.exe --config "C:\Users\YourName\OpenVPN\config\client1.ovpn"
# Connect in background (as a Windows service)
# Use the GUI or: sc start OpenVPNService
Verify on Windows
# In PowerShell or cmd
ipconfig # look for a "TAP-Windows Adapter" or "OpenVPN Wintun"
# with a 10.8.0.x address
route print # check routing table for VPN routes
# Check public IP
curl https://ifconfig.me
08 — Client Setup — macOS & Mobile
macOS — Tunnelblick
# 1. Install Tunnelblick from https://tunnelblick.net
# 2. Double-click client1.ovpn — Tunnelblick imports it automatically
# 3. Click the Tunnelblick menu bar icon → Connect client1
# 4. Verify: System Preferences → Network — a new VPN interface appears
# Command line via Tunnelblick's openvpn binary:
sudo /Applications/Tunnelblick.app/Contents/Resources/openvpn/openvpn-*/openvpn \
--config ~/client1.ovpn
iOS — OpenVPN Connect
# 1. Install "OpenVPN Connect" from the App Store
# 2. Transfer client1.ovpn to the device via:
# - AirDrop from Mac
# - Email attachment (open with OpenVPN Connect)
# - Files app from a cloud storage folder
# 3. Tap the imported profile → Add → Connect
Android — OpenVPN Connect
# 1. Install "OpenVPN Connect" from Google Play
# 2. Transfer client1.ovpn to the device via:
# - USB file transfer
# - Cloud storage (Google Drive, etc.)
# 3. Open the app → + → File → select client1.ovpn → Import → Connect
09 — Full Tunnel vs Split Tunnel
The routing mode is controlled entirely by directives pushed from the server to the client. No client config change is needed when switching between modes.
Full tunnel — all client traffic through the VPN
# In server.conf — push these directives to clients:
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
# Effect:
# Client default route → VPN → server → internet
# All DNS queries resolve through the server's DNS
# Client's real IP is hidden from the internet
# All client traffic flows through your server (use for privacy, remote work)
Split tunnel — only specific subnets through the VPN
# In server.conf — remove redirect-gateway, push specific routes:
# push "redirect-gateway def1 bypass-dhcp" ← comment this out
push "route 192.168.1.0 255.255.255.0" # reach the office LAN
push "route 10.0.0.0 255.255.0.0" # reach internal servers
push "dhcp-option DNS 192.168.1.1" # use internal DNS for VPN traffic
# Effect:
# Traffic to 192.168.1.x and 10.0.x.x → VPN
# Everything else → client's normal internet connection
# Lower load on the VPN server
# Client retains local internet speed for browsing
Per-client routing overrides
# Push different routes to a specific client using a client config directory
# In server.conf:
client-config-dir /etc/openvpn/server/ccd
# /etc/openvpn/server/ccd/client1
# Assign a fixed IP to client1
ifconfig-push 10.8.0.10 10.8.0.11
# Push an extra route only to client1
push "route 172.16.0.0 255.255.0.0"
# /etc/openvpn/server/ccd/client2
ifconfig-push 10.8.0.20 10.8.0.21
ℹ Fixed IP assignments via ccd are useful when a client is a server itself — you always know its VPN IP and can write firewall rules based on it.
10 — Server Management
Monitor connected clients
# Read the status file (updates every 60s by default)
sudo cat /var/log/openvpn/openvpn-status.log
# Follow the main log
sudo tail -f /var/log/openvpn/openvpn.log
# Via systemd journal
sudo journalctl -u openvpn-server@server -f
# List active connections using the management interface (if enabled)
# Add to server.conf: management localhost 7505
echo "status" | nc localhost 7505
Enable the management interface
# In server.conf — add:
management localhost 7505
management-client-auth # optional: require client auth on management socket
# Restart server to activate
sudo systemctl restart openvpn-server@server
# Connect and issue commands
nc localhost 7505
# Commands:
# status — list connected clients
# kill client1 — forcibly disconnect a client
# log 10 — show last 10 log lines
# quit — exit
Forcibly disconnect a client
# Via management interface
echo "kill client1" | nc localhost 7505
# Or by revoking their cert and reloading (permanent)
cd ~/openvpn-ca
./easyrsa revoke client1
./easyrsa gen-crl
sudo cp pki/crl.pem /etc/openvpn/server/
sudo systemctl reload openvpn-server@server
Reload config without dropping connections
# SIGHUP causes OpenVPN to re-read its config and CRL
sudo systemctl reload openvpn-server@server
# or:
sudo kill -HUP $(pgrep -f "openvpn.*server")
11 — Advanced Server Options
Run OpenVPN on TCP 443 (bypass deep packet inspection)
# In server.conf:
port 443
proto tcp
# Note: TCP over TCP (VPN over TCP tunnel) can cause performance issues
# under packet loss. Only use TCP 443 if UDP is blocked.
Multiple OpenVPN instances on one server
# Create a second config file
sudo cp /etc/openvpn/server/server.conf /etc/openvpn/server/server-tcp.conf
# Edit server-tcp.conf — change port and proto
port 443
proto tcp
server 10.9.0.0 255.255.255.0 # different VPN subnet
status /var/log/openvpn/openvpn-tcp-status.log
# Start second instance
sudo systemctl enable --now openvpn-server@server-tcp
ECDH — drop static DH parameters (OpenVPN 2.4.8+)
# In server.conf — replace "dh dh.pem" with:
dh none
ecdh-curve secp384r1
# No need to generate or distribute dh.pem
# Perfect Forward Secrecy via ECDHE key exchange
Two-factor authentication with OTP
# Install google-authenticator PAM module
sudo apt install libpam-google-authenticator
# In server.conf — add:
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn
reneg-sec 0
# In client.ovpn — add:
auth-user-pass # prompts for username + OTP as password
# Configure PAM for OpenVPN:
# /etc/pam.d/openvpn
auth required pam_google_authenticator.so
Restrict which clients can connect (certificate CN matching)
# In server.conf — only allow certs with matching CN patterns:
tls-verify "/etc/openvpn/server/verify-cn.sh"
# /etc/openvpn/server/verify-cn.sh
#!/bin/bash
# $1 = depth, $2 = CN
if [ "$1" = "0" ]; then
case "$2" in
client*) exit 0 ;; # allow any CN starting with "client"
*) exit 1 ;;
esac
fi
exit 0
12 — Firewall Integration
On the server, three things need to be in place: the VPN port must be open inbound, IP forwarding must be enabled, and the VPN subnet must be NAT-masqueraded through the server's internet interface.
ufw — complete server firewall setup
# Allow OpenVPN UDP port
sudo ufw allow 1194/udp
# Allow forwarding — edit /etc/default/ufw:
DEFAULT_FORWARD_POLICY="ACCEPT"
# Add NAT rule at the top of /etc/ufw/before.rules (before *filter):
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
# Enable ufw forwarding in /etc/ufw/sysctl.conf:
net/ipv4/ip_forward=1
# Reload
sudo ufw disable && sudo ufw enable
firewalld — complete server firewall setup
# Allow OpenVPN service
sudo firewall-cmd --zone=public --add-service=openvpn --permanent
# Enable masquerade on the external zone
sudo firewall-cmd --zone=external --add-masquerade --permanent
# Add VPN interface to the internal zone
sudo firewall-cmd --zone=internal --add-interface=tun0 --permanent
# Allow forwarding from VPN zone to external
sudo firewall-cmd --new-policy=vpn-to-wan --permanent
sudo firewall-cmd --policy=vpn-to-wan --add-ingress-zone=internal --permanent
sudo firewall-cmd --policy=vpn-to-wan --add-egress-zone=external --permanent
sudo firewall-cmd --policy=vpn-to-wan --set-target=ACCEPT --permanent
sudo firewall-cmd --reload
nftables — complete server firewall setup
table inet firewall {
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state { established, related } accept
udp dport 1194 accept # OpenVPN
# SSH (adjust to your admin port)
tcp dport 22 ct state new accept
}
chain forward {
type filter hook forward priority filter; policy drop;
# Allow forwarding between tun0 and eth0
iifname "tun0" oifname "eth0" accept
iifname "eth0" oifname "tun0" ct state { established, related } accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat;
ip saddr 10.8.0.0/24 oifname "eth0" masquerade
}
}
13 — Troubleshooting
| Symptom | Cause & Fix |
| TLS handshake failed | Clock skew between client and server (certificates are time-sensitive) — sync NTP on both. Also check tls-crypt key direction (server=0, client must use key-direction 1). |
| VERIFY ERROR: certificate not yet valid | System clock is wrong. Run timedatectl to check and sync NTP. |
| Connection established but no internet (full tunnel) | IP forwarding not enabled (sysctl net.ipv4.ip_forward must be 1) or NAT masquerade rule missing. |
| Connection established but can't reach LAN (split tunnel) | Missing push "route ..." directives in server.conf, or the LAN hosts have no route back to the VPN subnet. |
| DNS doesn't work through VPN | push "dhcp-option DNS ..." not in server.conf, or the client OS isn't applying the pushed DNS servers (common on Linux — see update-resolv-conf script). |
| Client connects then immediately drops | Check keepalive settings and firewall UDP timeout. Try TCP mode to rule out UDP filtering. |
| CERTIFICATE_VERIFY_FAILED | Wrong CA cert, cert expired, or CRL is outdated — regenerate with easyrsa gen-crl and copy to server. |
| Cannot connect — no response from server | UDP port 1194 blocked by firewall. Try TCP 443. Verify with: nc -u server-ip 1194. |
| Multiple clients: one drops when another connects | Missing duplicate-cn directive (same cert used twice) or IP pool exhausted. |
| tun0 does not appear on client | TUN kernel module not loaded — sudo modprobe tun. On containers, verify TUN device is available. |
Diagnostic commands
# Run OpenVPN with maximum verbosity (run as test — not in production)
sudo openvpn --config server.conf --verb 9
# Check TUN module is loaded
lsmod | grep tun
sudo modprobe tun
# Verify server is listening
sudo ss -ulnp | grep 1194 # UDP
sudo ss -tlnp | grep 1194 # TCP
# Check IP forwarding
sysctl net.ipv4.ip_forward
# View active NAT rules
sudo iptables -t nat -L -v -n
sudo nft list table ip nat
# Trace a packet through the tunnel
sudo tcpdump -i tun0
# Test UDP connectivity to the server from a client machine
nc -u server-ip 1194
14 — Quick Reference
| Command | What it does |
| ./easyrsa init-pki | Initialise a new PKI directory |
| ./easyrsa build-ca | Create the Certificate Authority |
| ./easyrsa gen-req server nopass | Generate server key + CSR |
| ./easyrsa sign-req server server | Sign the server CSR with the CA |
| ./easyrsa gen-req clientN nopass | Generate a client key + CSR |
| ./easyrsa sign-req client clientN | Sign a client CSR with the CA |
| ./easyrsa revoke clientN | Revoke a client certificate |
| ./easyrsa gen-crl | Regenerate the Certificate Revocation List |
| openvpn --genkey secret ta.key | Generate the tls-crypt pre-shared key |
| systemctl enable --now openvpn-server@server | Start and enable the OpenVPN server |
| systemctl status openvpn-server@server | Check server status |
| systemctl reload openvpn-server@server | Reload config / CRL without dropping connections |
| sudo openvpn --config client1.ovpn | Connect a Linux client (foreground) |
| systemctl enable --now openvpn-client@client1 | Connect a Linux client as a persistent service |
| journalctl -u openvpn-server@server -f | Follow server log |
✔ The complete setup order: build CA → generate server cert → generate DH + tls-crypt → configure server.conf → enable IP forwarding + NAT → open firewall port → start server → generate client cert → create .ovpn profile → test connection → disable password auth on clients → keep CA key offline.