You want a VPN that lives on your own box, terminates where you decide, and logs exactly what you tell it to log. OpenVPN 2.7 on Ubuntu 26.04 LTS (Resolute Raccoon) is still the most flexible way to do that: certificate auth, TLS-crypt on the control channel, AES-256-GCM data channel, and a plain .ovpn file you can hand to a laptop or phone.
This guide walks through a full deployment tested on two fresh Ubuntu 26.04 servers. We build the PKI with Easy-RSA 3.2, write /etc/openvpn/server/server.conf, wire up UFW with a NAT rule for 10.8.0.0/24, push DNS to clients, and bring a second machine up as a verified client on tun0. You will also see how to add more clients without rebuilding the CA, revoke a cert through the CRL, and read the logs when something refuses to connect.
Tested April 2026 on Ubuntu 26.04 LTS (kernel 7.0.0-10), OpenVPN 2.7.0, Easy-RSA 3.2.5, OpenSSL 3.5.5
What you need before you start
Two Ubuntu 26.04 LTS hosts. One is the VPN server with a public, routable IP that your clients can reach on UDP 1194. The other is any machine you want to test from. Throughout the article the public endpoint is shown as 203.0.113.20 (RFC 5737 documentation range). Swap in your own public IP or DNS name wherever you see it.
- Two Ubuntu 26.04 LTS servers, tested on kernel 7.0.0-10-generic
- Root or sudo access on both hosts
- A public IPv4 on the VPN server, with UDP 1194 reachable from the internet
- UFW available (installed by default on the server images)
- Basic familiarity with systemd units and
journalctl
If you have not yet done the baseline hardening, run through our Ubuntu 26.04 initial server setup and Ubuntu 26.04 server hardening guide first. Both are worth ten minutes before you expose a service on UDP 1194.
Step 1: Install OpenVPN and Easy-RSA
Both packages ship in the default Ubuntu 26.04 archive. No third-party PPA needed.
sudo apt update
sudo apt install -y openvpn easy-rsa ufw
Confirm the versions. On Ubuntu 26.04 you should see OpenVPN 2.7.x and Easy-RSA 3.2.x:
openvpn --version | head -1
dpkg -l | grep -E '^ii (openvpn|easy-rsa) ' | awk '{print $2, $3}'
The installed versions on the test host:
OpenVPN 2.7.0 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [AEAD] [DCO]
easy-rsa 3.2.5-1
openvpn 2.7.0-1ubuntu1
OpenVPN 2.7 links against OpenSSL 3.5.5 on Ubuntu 26.04 and enables DCO (Data Channel Offload) at compile time. See the OpenVPN 2.7 changelog for the full list of changes since 2.6.
Step 2: Build the PKI with Easy-RSA
OpenVPN is certificate-authenticated. That means a small private CA, a server cert, one cert per client, and a TLS-crypt static key that gates the control channel before any TLS handshake starts. Easy-RSA 3 wraps all of this in a single script.
Create a working copy of Easy-RSA under /etc/openvpn/ so the scripts run with the right paths:
sudo mkdir -p /etc/openvpn/easy-rsa
sudo ln -sf /usr/share/easy-rsa/* /etc/openvpn/easy-rsa/
cd /etc/openvpn/easy-rsa
Drop a vars file so every cert uses the same subject defaults. We pick ECDSA on the secp384r1 curve here. It is faster, keys are smaller, and Easy-RSA 3.2 supports it out of the box:
sudo tee /etc/openvpn/easy-rsa/vars > /dev/null <<'EOF'
set_var EASYRSA_ALGO ec
set_var EASYRSA_CURVE secp384r1
set_var EASYRSA_REQ_COUNTRY "US"
set_var EASYRSA_REQ_PROVINCE "Example"
set_var EASYRSA_REQ_CITY "ExampleCity"
set_var EASYRSA_REQ_ORG "computingforgeeks"
set_var EASYRSA_REQ_EMAIL "[email protected]"
set_var EASYRSA_REQ_OU "VPN"
set_var EASYRSA_CERT_EXPIRE 3650
set_var EASYRSA_CA_EXPIRE 3650
EOF
Initialize the PKI directory and build the CA. nopass leaves the CA private key unencrypted on disk. That is fine for a lab or a dedicated VPN box; on shared hardware, remove nopass and enter a passphrase.
sudo ./easyrsa init-pki
sudo EASYRSA_BATCH=1 ./easyrsa --req-cn="OpenVPN-CA" build-ca nopass
Easy-RSA prints the path to the CA cert it just wrote:
Notice
------
CA creation complete. Your new CA certificate is at:
* /etc/openvpn/easy-rsa/pki/ca.crt
Build-ca completed successfully.
Issue the server certificate and the first client certificate. Both are signed by the CA we just created:
sudo ./easyrsa --batch build-server-full server nopass
sudo ./easyrsa --batch build-client-full client1 nopass
Generate the initial CRL. Even if you never plan to revoke anything, OpenVPN refuses to start with crl-verify pointing at a file that does not exist:
sudo ./easyrsa gen-crl
Finally, generate the TLS-crypt static key. This key encrypts and authenticates every packet on the control channel, so a network attacker cannot even see the TLS handshake start, let alone fingerprint it:
sudo openvpn --genkey tls-crypt /etc/openvpn/easy-rsa/pki/tc.key
At this point pki/ contains everything the server and the clients need:
sudo ls /etc/openvpn/easy-rsa/pki/issued /etc/openvpn/easy-rsa/pki/private
The two directories should list the server cert, the client1 cert, and the matching private keys:
/etc/openvpn/easy-rsa/pki/issued:
client1.crt server.crt
/etc/openvpn/easy-rsa/pki/private:
ca.key client1.key server.key

Step 3: Write the OpenVPN server config
Copy only the files the server process needs into /etc/openvpn/server/. Do not symlink into pki/. Keeping a clean copy means you can back up the PKI separately and rotate it without breaking a running daemon:
cd /etc/openvpn/easy-rsa/pki
sudo cp ca.crt issued/server.crt private/server.key tc.key crl.pem /etc/openvpn/server/
sudo chown nobody:nogroup /etc/openvpn/server/crl.pem
The crl.pem ownership matters because OpenVPN drops to the nobody user after startup and still needs to read the CRL on every new connection.
Now write the server config. This is a full-tunnel setup on 10.8.0.0/24 over UDP 1194, with modern ciphers and no legacy fallbacks:
sudo nano /etc/openvpn/server/server.conf
Paste the following content. Each block maps directly to one decision: transport, addresses, what to push, authentication, logging.
port 1194
proto udp4
dev tun
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/server.crt
key /etc/openvpn/server/server.key
dh none
topology subnet
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 9.9.9.9"
keepalive 10 120
tls-crypt /etc/openvpn/server/tc.key
cipher AES-256-GCM
auth SHA256
data-ciphers AES-256-GCM:AES-128-GCM
user nobody
group nogroup
persist-key
persist-tun
crl-verify /etc/openvpn/server/crl.pem
status /var/log/openvpn/openvpn-status.log
log-append /var/log/openvpn/openvpn.log
verb 3
explicit-exit-notify 1
A few of those directives are worth calling out. dh none is correct because we use ECDSA certs; there is no classic Diffie-Hellman exchange. tls-crypt replaces the older tls-auth by both signing and encrypting control packets, which hides the OpenVPN fingerprint on the wire. data-ciphers is the 2.6+ negotiated list; cipher stays for backward compatibility with any 2.4 client that might still connect.
Create the log directory the unit writes to:
sudo mkdir -p /var/log/openvpn
Step 4: Enable IP forwarding and UFW NAT
OpenVPN by itself does not route anything. The kernel has to forward packets between tun0 and your public interface, and UFW has to apply a masquerade on the way out. Both pieces are required for clients to see the internet through the tunnel.
Enable IPv4 forwarding persistently:
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-openvpn.conf
sudo sysctl -p /etc/sysctl.d/99-openvpn.conf
The sysctl -p should confirm the new value:
net.ipv4.ip_forward = 1
Detect the external interface name. On fresh Ubuntu 26.04 cloud images it is usually eth0, but do not assume:
ip route get 1.1.1.1 | awk '{print $5; exit}'
Substitute the returned interface name into the NAT rule below. Open /etc/ufw/before.rules and append a POSTROUTING masquerade block:
sudo nano /etc/ufw/before.rules
At the very bottom of the file, add the following stanza (replace eth0 with your interface):
# START OPENVPN RULES
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
# END OPENVPN RULES
Flip the default forward policy so UFW does not drop traffic exiting tun0:
sudo sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
Open SSH and the OpenVPN port, then enable UFW:
sudo ufw allow 22/tcp
sudo ufw allow 1194/udp
sudo ufw --force enable
sudo ufw status verbose
UFW should list both ports allowed and the routed forward default:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), allow (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
1194/udp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
1194/udp (v6) ALLOW IN Anywhere (v6)
For a deeper walkthrough of UFW profiles, application groups, and logging tiers, read our UFW firewall guide for Ubuntu 26.04.
Step 5: Start the OpenVPN server
Ubuntu 26.04 ships a templated unit at /usr/lib/systemd/system/[email protected]. The instance name after the @ matches the config filename under /etc/openvpn/server/. We named ours server.conf, so the unit is openvpn-server@server.
sudo systemctl enable --now openvpn-server@server
sudo systemctl status openvpn-server@server --no-pager
The status line you want to see is Status: "Initialization Sequence Completed". That means the certs loaded, the tun device came up, and the daemon is listening:
● [email protected] - OpenVPN service for server
Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled)
Active: active (running) since Tue 2026-04-14 20:37:51 UTC
Main PID: 3533 (openvpn)
Status: "Initialization Sequence Completed"
Tasks: 1 (limit: 1499)
Memory: 2M (peak: 2.2M)
Verify the tun0 interface carries the first IP in the VPN subnet and UDP 1194 is bound:
ip -brief addr show tun0
sudo ss -lun | grep 1194
The output should look like this:
tun0 UNKNOWN 10.8.0.1/24 fe80::1eaf:7d79:9909:7fe/64
UNCONN 0 0 0.0.0.0:1194 0.0.0.0:*

Step 6: Build the client .ovpn file
An inline .ovpn file bundles the CA, the client cert, the client key, and the TLS-crypt key into one text file. Clients import it without touching any other path. Build it on the server so you have the private key material in one place:
cd /etc/openvpn/easy-rsa/pki
sudo bash -c '
CA=$(cat ca.crt)
CERT=$(sed -ne "/BEGIN CERTIFICATE/,/END CERTIFICATE/p" issued/client1.crt)
KEY=$(cat private/client1.key)
TC=$(cat tc.key)
cat > /root/client1.ovpn <<EOF
client
dev tun
proto udp
remote 203.0.113.20 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
data-ciphers AES-256-GCM:AES-128-GCM
verb 3
<ca>
$CA
</ca>
<cert>
$CERT
</cert>
<key>
$KEY
</key>
<tls-crypt>
$TC
</tls-crypt>
EOF'
Replace 203.0.113.20 with the public IP or FQDN your clients will reach. The finished file lives at /root/client1.ovpn and is the only thing you need to copy to a client machine.
Step 7: Connect a client
On a second Ubuntu 26.04 machine, install the OpenVPN package:
sudo apt install -y openvpn
Copy the .ovpn file from the server. Rename it client.conf and drop it under /etc/openvpn/client/ so the systemd template unit picks it up:
scp [email protected]:/root/client1.ovpn /tmp/client1.ovpn
sudo install -m 600 /tmp/client1.ovpn /etc/openvpn/client/client.conf
Start the matching unit. As with the server, the name after @ matches the config filename:
sudo systemctl enable --now openvpn-client@client
sudo systemctl is-active openvpn-client@client
Tail the last few lines of the journal to confirm the handshake completed:
sudo journalctl -u openvpn-client@client --no-pager | tail -5
The last line should read Initialization Sequence Completed:
client[2749]: TUN/TAP device tun0 opened
client[2749]: /sbin/ip link set dev tun0 up mtu 1500
client[2749]: /sbin/ip addr add dev tun0 10.8.0.2/24 broadcast 10.8.0.255
client[2749]: WARNING: this configuration may cache passwords in memory
client[2749]: Initialization Sequence Completed
Verify the tunnel is carrying real traffic. The client receives the next free IP from the pool, 10.8.0.2, and can ping the server side of the tunnel at 10.8.0.1:
ip -brief addr show tun0
ping -c 3 10.8.0.1
The three ICMP replies confirm both routing and encryption are working end to end:
tun0 UNKNOWN 10.8.0.2/24 fe80::45bb:23c3:27f1:e3ed/64
PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data.
64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=0.503 ms
64 bytes from 10.8.0.1: icmp_seq=2 ttl=64 time=0.701 ms
64 bytes from 10.8.0.1: icmp_seq=3 ttl=64 time=0.611 ms
--- 10.8.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Confirm your public IP now matches the VPN server. From the client:
curl -s https://ifconfig.me; echo
The address returned should be the server’s public IP, not the client’s ISP address. If it still shows the ISP, the NAT rule or the default forward policy is not what Step 4 built.
Adding more clients
You do not rebuild the CA to onboard a new user. Sign the new cert against the existing PKI and render a fresh .ovpn:
cd /etc/openvpn/easy-rsa
sudo ./easyrsa --batch build-client-full alice nopass
Regenerate the inline profile by running the same heredoc script from Step 6 with alice in place of client1. Hand the resulting alice.ovpn to the user on a channel you trust (GPG-encrypted email, a password manager, a short-lived file share). The server needs no restart; OpenVPN validates the client cert against the CA and the current CRL at connect time.
Revoking a client
When a laptop is stolen or a contract ends, revoke the cert and refresh the CRL. The OpenVPN server reads the updated CRL on the next connection attempt:
cd /etc/openvpn/easy-rsa
sudo ./easyrsa --batch revoke alice
sudo ./easyrsa gen-crl
sudo cp pki/crl.pem /etc/openvpn/server/crl.pem
sudo chown nobody:nogroup /etc/openvpn/server/crl.pem
sudo systemctl restart openvpn-server@server
Easy-RSA confirms the revocation and reminds you to publish the new CRL:
Revocation was successful. You must run 'gen-crl' and upload
a new CRL to your infrastructure in order to prevent the revoked
certificate from being accepted.
The CRL has an expiry baked in (default 180 days in Easy-RSA 3.2). Set a reminder to regenerate it before that window closes, or the server will refuse all new connections.
Logs and troubleshooting
OpenVPN writes to /var/log/openvpn/openvpn.log because we set log-append in the config, and it also goes to the systemd journal through the unit. When something breaks, check both.
The client hangs on “TLS handshake failed”
This almost always means UDP 1194 is not reaching the server. Confirm with tcpdump on the server’s public interface while the client tries to connect:
sudo tcpdump -ni eth0 udp port 1194
If you see no packets, a cloud provider security group or upstream firewall is dropping them. If you see packets arriving but the client still times out, UFW is blocking them (re-check Step 4) or the OpenVPN process is not bound (re-check ss -lun | grep 1194).
Error: “VERIFY ERROR: depth=0, error=CRL has expired”
The CRL file aged past its validity window. Regenerate it and copy it back into place:
cd /etc/openvpn/easy-rsa
sudo ./easyrsa gen-crl
sudo cp pki/crl.pem /etc/openvpn/server/crl.pem
sudo chown nobody:nogroup /etc/openvpn/server/crl.pem
sudo systemctl restart openvpn-server@server
Clients connect but cannot reach the internet
The tunnel is up but NAT is not. Confirm IP forwarding is on and the masquerade rule is loaded:
sysctl net.ipv4.ip_forward
sudo iptables -t nat -L POSTROUTING -n -v
You want to see net.ipv4.ip_forward = 1 and a MASQUERADE line with source 10.8.0.0/24 and out-interface matching your public NIC. If the MASQUERADE rule is missing, UFW never loaded the before.rules stanza; run sudo ufw disable && sudo ufw enable to reload.
Checking who is connected right now
OpenVPN updates /var/log/openvpn/openvpn-status.log every minute with the live client list:
sudo cat /var/log/openvpn/openvpn-status.log
Each active peer appears as a CLIENT_LIST row with its common name, real IP, virtual IP, byte counters, and connection time. Handy for capacity checks and for catching a client that is still connected from an old cert you thought you revoked.
How OpenVPN compares to WireGuard on Ubuntu 26.04
OpenVPN is not the only option on modern Ubuntu. If you have not already, read our WireGuard guide for Ubuntu 26.04. The short version, based on hands-on deployments of both:
| Concern | OpenVPN 2.7 | WireGuard |
|---|---|---|
| Config model | Certificates + CA + CRL | Static public/private keys per peer |
| Default port | UDP 1194 (configurable) | UDP 51820 (configurable) |
| Handshake visibility on the wire | Hidden behind tls-crypt | Obfuscated, no response to unsolicited packets |
| Throughput on a small VM | Good; DCO brings it closer to WireGuard | Excellent, kernel-level |
| Client availability | Desktop, mobile, routers, everywhere | Native on recent kernels, apps on all major platforms |
| Policy features (per-user push, DNS, routes) | Rich, via server directives | Minimal, handled in PostUp scripts |
| Revocation | CRL-based, reversible through PKI | Delete the peer from the config and reload |
If all you run is a small personal tunnel between machines you control, WireGuard is faster to stand up and faster on the wire. If you need per-user auth, granular pushes, centrally-managed certificates, or compatibility with a legacy client, OpenVPN still wins.
Where to go from here
A basic OpenVPN server is useful on day one; a hardened, routed, monitored one is what you want running a month later. Good follow-ups from here:
- Put a reverse-routed service behind the VPN and reach it through the tunnel. Our Nginx with Let’s Encrypt on Ubuntu 26.04 guide pairs well for an internal dashboard on
10.8.0.x - Run the VPN host as a Docker host too. The Docker CE on Ubuntu 26.04 guide walks through the setup
- Scrape
openvpn-status.logwith a Prometheus exporter and alert on stalled clients; our Prometheus on Ubuntu 26.04 guide covers the server side - Roll the CA and TLS-crypt key every 12-24 months, or after any suspected key exposure
- Review the full Ubuntu 26.04 LTS feature list for kernel and networking changes relevant to VPN hosts
That is a full, working OpenVPN 2.7 deployment on Ubuntu 26.04 with real PKI, real firewall rules, real revocation, and a real client on the other end. Keep the CA offline once it is built, rotate client certs on a schedule, and you have a VPN you can actually trust.