Introduction
Remote access VPN is critical infrastructure—it’s often the only way employees access internal resources. After deploying OpenVPN across multiple organizations, I’ve developed a hardened configuration that balances security with usability.
This guide covers a production-ready OpenVPN setup with certificate-based authentication, proper key management, and integration with existing identity providers.
Architecture Overview
┌─────────────────────┐
│ Load Balancer │
│ (Optional HA) │
└──────────┬──────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
│ OpenVPN 01 │ │ OpenVPN 02 │ │ OpenVPN 03 │
│ (Primary) │ │ (Secondary) │ │ (Tertiary) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└────────────────────┴────────────────────┘
│
┌──────────▼──────────┐
│ Internal Network │
│ (10.0.0.0/8) │
└─────────────────────┘
Server Setup
Prerequisites
# Ubuntu 22.04 LTS
apt update && apt upgrade -y
apt install openvpn easy-rsa ufw -y
Certificate Authority Setup
Never use the default easy-rsa configuration. Create a proper PKI:
# Initialize PKI
make-cadir /etc/openvpn/easy-rsa
cd /etc/openvpn/easy-rsa
# Configure vars
cat > vars << 'EOF'
set_var EASYRSA_REQ_COUNTRY "US"
set_var EASYRSA_REQ_PROVINCE "California"
set_var EASYRSA_REQ_CITY "San Francisco"
set_var EASYRSA_REQ_ORG "Your Organization"
set_var EASYRSA_REQ_EMAIL "security@yourorg.com"
set_var EASYRSA_REQ_OU "Infrastructure"
set_var EASYRSA_KEY_SIZE 4096
set_var EASYRSA_ALGO ec
set_var EASYRSA_CURVE secp384r1
set_var EASYRSA_CA_EXPIRE 3650
set_var EASYRSA_CERT_EXPIRE 365
set_var EASYRSA_CRL_DAYS 180
EOF
# Initialize and build CA
./easyrsa init-pki
./easyrsa build-ca nopass
Generate Server Certificate
# Generate server keypair
./easyrsa gen-req vpn-server nopass
./easyrsa sign-req server vpn-server
# Generate DH parameters (takes a while)
./easyrsa gen-dh
# Generate TLS auth key
openvpn --genkey secret /etc/openvpn/ta.key
# Copy files to OpenVPN directory
cp pki/ca.crt /etc/openvpn/
cp pki/issued/vpn-server.crt /etc/openvpn/
cp pki/private/vpn-server.key /etc/openvpn/
cp pki/dh.pem /etc/openvpn/
Server Configuration
# /etc/openvpn/server.conf
# Network Configuration
port 1194
proto udp
dev tun
topology subnet
# Certificates
ca /etc/openvpn/ca.crt
cert /etc/openvpn/vpn-server.crt
key /etc/openvpn/vpn-server.key
dh /etc/openvpn/dh.pem
tls-auth /etc/openvpn/ta.key 0
# VPN Subnet
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist /var/log/openvpn/ipp.txt
# Push routes to clients
push "route 10.0.0.0 255.0.0.0"
push "route 172.16.0.0 255.240.0.0"
# DNS (push internal DNS)
push "dhcp-option DNS 10.0.0.53"
push "dhcp-option DOMAIN internal.yourorg.com"
# Security Hardening
cipher AES-256-GCM
auth SHA512
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384
tls-version-min 1.2
# Certificate Verification
remote-cert-tls client
verify-x509-name vpn-client name-prefix
# Connection Management
keepalive 10 120
max-clients 100
persist-key
persist-tun
# Logging
status /var/log/openvpn/status.log
log-append /var/log/openvpn/openvpn.log
verb 3
mute 20
# Drop privileges
user nobody
group nogroup
Firewall Configuration
# Enable IP forwarding
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sysctl -p
# UFW configuration
ufw allow 1194/udp
ufw allow OpenSSH
# NAT for VPN clients
cat >> /etc/ufw/before.rules << 'EOF'
# NAT for OpenVPN
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
EOF
ufw enable
Client Certificate Management
Generating Client Certificates
# Generate client certificate
cd /etc/openvpn/easy-rsa
./easyrsa gen-req client-john nopass
./easyrsa sign-req client client-john
Client Configuration Template
# client.ovpn template
client
dev tun
proto udp
remote vpn.yourorg.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
# Security
cipher AES-256-GCM
auth SHA512
tls-client
remote-cert-tls server
# Prevent DNS leaks
block-outside-dns
# Certificates (embedded)
<ca>
-----BEGIN CERTIFICATE-----
[CA certificate content]
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
[Client certificate content]
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
[Client private key content]
-----END PRIVATE KEY-----
</key>
<tls-auth>
-----BEGIN OpenVPN Static key V1-----
[TLS auth key content]
-----END OpenVPN Static key V1-----
</tls-auth>
key-direction 1
Automated Client Generation Script
#!/bin/bash
# generate-client.sh
CLIENT_NAME=$1
OVPN_DIR="/etc/openvpn"
EASYRSA_DIR="/etc/openvpn/easy-rsa"
OUTPUT_DIR="/home/vpn-admin/clients"
if [ -z "$CLIENT_NAME" ]; then
echo "Usage: $0 <client-name>"
exit 1
fi
# Generate certificate
cd $EASYRSA_DIR
./easyrsa gen-req $CLIENT_NAME nopass
./easyrsa sign-req client $CLIENT_NAME
# Build .ovpn file
cat > $OUTPUT_DIR/$CLIENT_NAME.ovpn << EOF
client
dev tun
proto udp
remote vpn.yourorg.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-GCM
auth SHA512
remote-cert-tls server
block-outside-dns
verb 3
<ca>
$(cat $EASYRSA_DIR/pki/ca.crt)
</ca>
<cert>
$(cat $EASYRSA_DIR/pki/issued/$CLIENT_NAME.crt)
</cert>
<key>
$(cat $EASYRSA_DIR/pki/private/$CLIENT_NAME.key)
</key>
<tls-auth>
$(cat $OVPN_DIR/ta.key)
</tls-auth>
key-direction 1
EOF
chmod 600 $OUTPUT_DIR/$CLIENT_NAME.ovpn
echo "Client config created: $OUTPUT_DIR/$CLIENT_NAME.ovpn"
Split Tunneling
Not all traffic needs to go through the VPN. Configure split tunneling for better performance:
# Server config - only push internal routes
push "route 10.0.0.0 255.0.0.0"
push "route 172.16.0.0 255.240.0.0"
# Don't push: redirect-gateway def1
# Client config - if you need full tunnel for specific clients
# Add this to their .ovpn:
# redirect-gateway def1 bypass-dhcp
Per-Client Configuration
# /etc/openvpn/ccd/client-john
# Give John a static IP
ifconfig-push 10.8.0.50 255.255.255.0
# Push additional routes for John (admin access)
push "route 192.168.100.0 255.255.255.0"
Multi-Factor Authentication
Integration with LDAP/AD
# Install PAM plugin
apt install openvpn-auth-ldap
# /etc/openvpn/auth-ldap.conf
<LDAP>
URL ldaps://ldap.yourorg.com
BindDN cn=vpn-bind,ou=service,dc=yourorg,dc=com
Password your-bind-password
Timeout 15
TLSEnable yes
TLSCACertFile /etc/ssl/certs/ca-certificates.crt
</LDAP>
<Authorization>
BaseDN "ou=users,dc=yourorg,dc=com"
SearchFilter "(&(uid=%u)(memberOf=cn=vpn-users,ou=groups,dc=yourorg,dc=com))"
RequireGroup true
</Authorization>
Add to server.conf:
plugin /usr/lib/openvpn/openvpn-auth-ldap.so /etc/openvpn/auth-ldap.conf
TOTP Integration
# Install Google Authenticator PAM
apt install libpam-google-authenticator
# Configure PAM for OpenVPN
# /etc/pam.d/openvpn
auth required pam_google_authenticator.so
# Add to server.conf
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn
Monitoring and Logging
Status Monitoring
# Parse OpenVPN status file
#!/bin/bash
# vpn-status.sh
STATUS_FILE="/var/log/openvpn/status.log"
echo "=== Connected Clients ==="
awk '/^CLIENT_LIST/{print $2, $3, $5}' $STATUS_FILE | column -t
echo ""
echo "=== Connection Stats ==="
echo "Total connected: $(grep -c '^CLIENT_LIST' $STATUS_FILE)"
Centralized Logging
# Send logs to syslog/SIEM
# /etc/rsyslog.d/openvpn.conf
:programname, isequal, "openvpn" /var/log/openvpn/openvpn.log
:programname, isequal, "openvpn" @siem.yourorg.com:514
Connection Events Script
# /etc/openvpn/scripts/client-connect.sh
#!/bin/bash
logger -t openvpn "Client connected: $common_name from $trusted_ip"
# Notify Slack
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"VPN: $common_name connected from $trusted_ip\"}" \
https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# Add to server.conf
script-security 2
client-connect /etc/openvpn/scripts/client-connect.sh
client-disconnect /etc/openvpn/scripts/client-disconnect.sh
Certificate Revocation
When employees leave or certificates are compromised:
# Revoke certificate
cd /etc/openvpn/easy-rsa
./easyrsa revoke client-john
./easyrsa gen-crl
# Copy CRL to OpenVPN
cp pki/crl.pem /etc/openvpn/
# Add to server.conf (if not already present)
crl-verify /etc/openvpn/crl.pem
# Restart OpenVPN
systemctl restart openvpn@server
Troubleshooting
Common Issues
# Check server logs
tail -f /var/log/openvpn/openvpn.log
# Test connectivity
nc -zvu vpn.yourorg.com 1194
# Verify certificates
openssl verify -CAfile /etc/openvpn/ca.crt /etc/openvpn/vpn-server.crt
# Check routes on client
ip route show | grep tun
Lessons Learned
- Certificate expiry is a production incident. Set up monitoring and automate renewals 30 days before expiry.
- Split tunneling improves user experience. Not everything needs VPN—let video calls and updates go direct.
- Log everything. VPN logs are critical for security audits and troubleshooting.
- Have a backup access method. When VPN is down, you need another way in (bastion host, console access).
- Test certificate revocation. Make sure CRL is working before you actually need it.
Conclusion
A well-configured OpenVPN server is infrastructure you can forget about—until certificate renewal time. Invest in proper PKI, automate certificate management, and monitor connection health.
The configuration above has served thousands of users across multiple organizations. Start with the basics, add MFA when ready, and always prioritize security over convenience.