
Setting up a VPN server sounds simple until you’re the one paged at 2 a.m. because a remote engineer can’t reach the internal database and the client config you generated three months ago suddenly throws a TLS handshake error. That’s the reality of running OpenVPN in production, not the sanitized ten-step tutorials that skip the parts where things actually break.
Ubuntu 26.04 LTS, codenamed “Resolute Raccoon,” changes a few things worth knowing before you touch a terminal. It ships with the Linux 7.0 kernel, which finally brings the in-tree ovpn kernel module out of experimental status, meaning data channel offload (DCO) is available without compiling anything extra. OpenVPN itself lands at version 2.7.0 in the default repositories, paired with Easy-RSA 3.2.5. That combination matters because DCO alone can cut CPU usage on the tunnel by a significant margin on busy servers, something anyone who has watched htop pin a single core during a traffic spike will appreciate.
This guide walks through a full production-grade OpenVPN deployment on Ubuntu 26.04 LTS: certificate authority setup with Easy-RSA, hardened server configuration, UFW firewall rules with proper NAT masquerading, systemd service management, client provisioning, and a troubleshooting section built from problems that actually show up in the field. Whether you’re securing a remote team’s access to internal infrastructure, building a personal VPN for privacy while working from a coffee shop in Yogyakarta, or replacing an aging PPTP setup that should have been retired a decade ago, the steps below apply. Root or sudo access, a public IP or domain pointing at the box, and an open UDP port are the only prerequisites. Everything else gets covered as we go.
Why OpenVPN Still Matters in 2026
WireGuard gets most of the hype these days, and rightfully so for raw throughput. But OpenVPN hasn’t gone anywhere, and there’s a reason enterprises keep deploying it. It runs over both UDP and TCP, which matters enormously when you’re trying to tunnel out of restrictive corporate networks or countries that throttle non-standard protocols. TCP mode can disguise VPN traffic as regular HTTPS on port 443, something WireGuard’s UDP-only design simply can’t do without extra obfuscation layers.
OpenVPN also has a mature, battle-tested certificate-based authentication model. For organizations that need granular per-user revocation, detailed audit logging, and compatibility with existing PKI infrastructure, that’s not a small thing. If a laptop gets stolen, you revoke one certificate and move on. No key rotation across an entire fleet required.
Prerequisites and Pre-Flight Checks
Before installing anything, confirm the basics. Skipping this step is how people end up debugging a firewall issue for an hour when the real problem was a missing DNS record.
Check your Ubuntu version:
lsb_release -a
You should see 26.04 in the output. If you’re still on 24.04 or 22.04, most of this guide applies equally, with minor differences noted where relevant.
Confirm you have a public-facing IP address or a domain name resolving to your server:
curl -4 ifconfig.me
Update the system fully before touching packages. Running OpenVPN on a stale kernel is asking for compatibility headaches, especially with the newer ovpn DCO module.
sudo apt update && sudo apt upgrade -y
sudo reboot
Reboot if a kernel update was applied. This matters more than people think, since the DCO module needs to match the running kernel.
Step 1: Installing OpenVPN and Easy-RSA
Once the reboot completes, install the core packages:
sudo apt update
sudo apt install -y openvpn easy-rsa ufw
Verify what got installed. This is a habit worth keeping, because relying on memory for version numbers when you’re troubleshooting six months later never works out.
openvpn --version | head -1
dpkg-query -W -f='${Package} ${Version}\n' openvpn easy-rsa
On Ubuntu 26.04, expect OpenVPN 2.7.0 and Easy-RSA 3.2.5. That OpenVPN version brings full support for the kernel’s native ovpn module, so the data plane can run inside the kernel instead of bouncing packets through userspace. On a mid-range VPS handling dozens of concurrent tunnels, that’s the difference between a load average that stays flat and one that climbs steadily as connections pile up.
Step 2: Building the Certificate Authority
OpenVPN’s security model leans entirely on PKI. Get this part wrong and everything downstream is compromised, so treat the CA directory like it holds the keys to the kingdom, because it does.
Create a working directory for Easy-RSA. Don’t build it directly inside /usr/share/easy-rsa, since that’s a shared system path and mixing your PKI data into it invites mistakes during upgrades.
sudo mkdir -p /etc/openvpn/easy-rsa
sudo make-cadir /etc/openvpn/easy-rsa
cd /etc/openvpn/easy-rsa
Initialize the PKI structure:
sudo ./easyrsa init-pki
Build the certificate authority. You’ll be prompted for a Common Name, anything identifiable works, like MyCompany-VPN-CA.
sudo ./easyrsa build-ca nopass
The nopass flag skips encrypting the CA private key with a passphrase. On a single-admin server that’s tightly access-controlled, this is a reasonable tradeoff for automation convenience. On a shared or higher-risk environment, drop nopass and accept the extra prompt each time you sign a certificate. There’s no universally correct answer here, it depends on your threat model and who else has shell access to the box.
Generate the server certificate request and sign it:
sudo ./easyrsa gen-req server nopass
sudo ./easyrsa sign-req server server
Type yes when prompted to confirm the signing request. Now generate the Diffie-Hellman parameters, which take a genuine minute or two depending on CPU. This step trips up a lot of people who assume the terminal has frozen. It hasn’t, DH parameter generation is just computationally heavy.
sudo ./easyrsa gen-dh
Generate a TLS crypt key for an additional HMAC layer against DoS attacks and TLS handshake fingerprinting:
sudo openvpn --genkey secret /etc/openvpn/easy-rsa/pki/ta.key
Step 3: Deploying Server Certificates
Create the target directory and copy the necessary files:
sudo mkdir -p /etc/openvpn/server
cd /etc/openvpn/easy-rsa
sudo cp pki/ca.crt pki/issued/server.crt pki/private/server.key pki/dh.pem pki/ta.key /etc/openvpn/server/
Lock down permissions on the private key immediately. Leaving it world-readable, even briefly, is the kind of oversight that ends up in a post-incident report.
sudo chmod 600 /etc/openvpn/server/server.key
Step 4: Writing the Server Configuration
Copy the sample config as a starting point rather than writing one from scratch:
sudo cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf /etc/openvpn/server/server.conf
Edit /etc/openvpn/server/server.conf with the following, adjusted for your environment:
port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
tls-crypt ta.key
topology subnet
server 10.8.0.0 255.255.255.0
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 1.0.0.1"
keepalive 10 120
cipher AES-256-GCM
auth SHA256
data-ciphers AES-256-GCM:AES-128-GCM
user nobody
group nogroup
persist-key
persist-tun
status /var/log/openvpn/openvpn-status.log
log-append /var/log/openvpn/openvpn.log
verb 3
explicit-exit-notify 1
A few of these lines deserve explanation, since blindly copying config values is how misconfigurations propagate across the internet.
proto udp is the right default for most deployments. UDP has lower overhead and no head-of-line blocking, which matters for latency-sensitive traffic. Switch to tcp on port 443 only if you’re specifically dealing with restrictive firewalls that block non-standard UDP ports, since TCP-over-TCP tunneling introduces its own performance penalties under packet loss.
data-ciphers AES-256-GCM:AES-128-GCM tells the server which ciphers it will negotiate with clients, replacing the older, now-deprecated cipher directive as the primary control (kept here for backward compatibility with older clients). GCM mode gives you authenticated encryption, meaning tampering gets detected without a separate HMAC pass, which is both faster and more secure than the old CBC-based setups from a decade ago.
user nobody and group nogroup drop privileges after startup. OpenVPN needs root briefly to bind the tun interface and read the certificate files, but running the actual data-forwarding process as an unprivileged user limits the blast radius if a vulnerability is ever exploited.
Create the log directory since it doesn’t exist by default:
sudo mkdir -p /var/log/openvpn
Step 5: Enabling IP Forwarding
Without this, the server will accept VPN connections but refuse to route traffic between the tunnel and the outside world. This is one of the most common reasons a “successful” connection still can’t reach the internet.
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-openvpn.conf
sudo sysctl -p /etc/sysctl.d/99-openvpn.conf
Step 6: Configuring UFW and NAT
Identify your primary network interface first, since the NAT rule needs it exactly right.
ip route get 1.1.1.1 | grep -oP 'dev \K\S+'
This usually returns something like eth0 or ens3. Note it down, you’ll reference it in the next step.
Open the required ports before enabling the firewall, otherwise you’ll lock yourself out mid-session, a mistake that has ended more than one remote sysadmin’s evening.
sudo ufw allow OpenSSH
sudo ufw allow 1194/udp
Now edit /etc/ufw/before.rules and add a NAT block right after the header comments, before the *filter section:
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
Replace eth0 with whatever interface you identified above. This is the single most common typo in OpenVPN setups, copy-pasting a tutorial’s interface name without checking your own.
Edit /etc/default/ufw and change:
DEFAULT_FORWARD_POLICY="ACCEPT"
Then apply everything:
sudo ufw enable
sudo ufw reload
sudo ufw status verbose
Cloud VPS users, this is where a surprising number of setups quietly fail. If you’re running on AWS, DigitalOcean, or similar, the platform’s security group or cloud firewall sits in front of UFW. Opening 1194/UDP in UFW alone does nothing if the cloud provider’s own firewall still blocks it. Always check both layers.
Step 7: Starting the OpenVPN Service
Ubuntu uses the openvpn-server@ systemd template unit, distinct from the older openvpn@ unit still present for backward compatibility.
sudo systemctl enable --now openvpn-server@server
sudo systemctl status openvpn-server@server --no-pager
You should see active (running). If it says failed, don’t guess, go straight to the logs:
sudo journalctl -u openvpn-server@server -n 50 --no-pager
Confirm the tun0 interface came up:
ip addr show tun0
Step 8: Generating Client Configurations
For each client, generate a unique certificate. Never reuse a certificate across multiple devices, that defeats the entire purpose of per-identity revocation.
cd /etc/openvpn/easy-rsa
sudo ./easyrsa gen-req client1 nopass
sudo ./easyrsa sign-req client client1
Build a .ovpn file that bundles everything the client needs into a single portable file:
mkdir -p ~/client-configs
cat /usr/share/doc/openvpn/examples/sample-config-files/client.conf > ~/client-configs/client1.ovpn
cat >> ~/client-configs/client1.ovpn <
$(cat /etc/openvpn/easy-rsa/pki/ca.crt)
$(cat /etc/openvpn/easy-rsa/pki/issued/client1.crt)
$(cat /etc/openvpn/easy-rsa/pki/private/client1.key)
$(cat /etc/openvpn/easy-rsa/pki/ta.key)
EOF
Transfer this file to the client securely, SCP over SSH, not email or Slack, and import it into the OpenVPN Connect app or use the CLI:
sudo openvpn --config client1.ovpn
Troubleshooting Common Issues
TLS handshake failure. Almost always a clock skew issue or a mismatched tls-crypt/tls-auth key between server and client. Check timedatectl status on both ends. Certificates are time-sensitive; a server five minutes off can silently reject otherwise valid handshakes.
Client connects but has no internet access. This is the IP forwarding or NAT masquerade rule, nine times out of ten. Double-check net.ipv4.ip_forward is actually 1 with sysctl net.ipv4.ip_forward, and re-verify the interface name in your UFW NAT rule matches reality.
Connection times out entirely. Usually a firewall problem, either UFW, the cloud provider’s security group, or an ISP blocking UDP. Test with sudo tcpdump -i any udp port 1194 on the server while attempting a connection from the client. If nothing shows up, the packet isn’t even reaching the box.
“AUTH_FAILED” errors. Check that the client certificate hasn’t been revoked and hasn’t expired. Also confirm you’re not accidentally reusing a client config whose certificate was regenerated with a different serial.
Server starts but tun0 never appears. Check for a conflicting process or a leftover config referencing a device name already in use. ip link show will reveal stale interfaces from a previous failed run.
High CPU usage under load. Confirm DCO is actually active. Run ip link show tun0 and look for the ovpn driver type, or check journalctl -u openvpn-server@server for a line confirming kernel data channel offload was negotiated. If it’s silently falling back to userspace mode, verify the kernel module is loaded with lsmod | grep ovpn.
Performance and Security Hardening
A few things separate a lab setup from something you’d trust with production traffic.
Enable DCO explicitly by confirming the kernel module loads at boot:
sudo modprobe ovpn
echo ovpn | sudo tee -a /etc/modules
Rotate the CRL (certificate revocation list) periodically and reference it in the server config with crl-verify crl.pem, so a stolen laptop or departed employee’s access can be cut off immediately rather than relying on manual cleanup.
Tune keepalive values based on your network. The default 10 120 works fine for most cases, but mobile clients on flaky cellular connections benefit from slightly shorter ping intervals to detect and recover from dropped tunnels faster.
Consider running fail2ban alongside OpenVPN’s logs to catch repeated failed authentication attempts, particularly if the server is internet-facing on a well-known port.
For high-throughput servers, watch disk I/O on the logging directory. Verbose logging (verb 4 or higher) during troubleshooting is fine temporarily, but leaving it on in production generates unnecessary disk writes that add up over months of uptime.