When OpenVPN is the right choice
For a new VPN that you control at both ends, WireGuard usually takes fewer steps. OpenVPN still makes sense in these cases:
- UDP is blocked. WireGuard only uses UDP. On a network that only lets outbound TCP through, OpenVPN in TCP mode on port 443 still gets past port-based filters. It doesn't disguise the traffic as HTTPS, and deep packet inspection can still recognise it, although the reference manual notes that
tls-cryptmakes OpenVPN harder to identify. - The devices already have an OpenVPN client. Managed laptops, routers and appliances often ship one, and you can't always install anything else on them.
- You want a certificate per person that you can revoke. Every client certificate is signed by your own certificate authority, and a revocation list shuts one device out without touching the others.
If none of these apply, WireGuard Cloud Deployment is shorter, and Tailscale doesn't need an open inbound port at all. If you want the same OpenVPN server with a web console for users, see Pritunl VPN Deployment.
What you need
- A cloud VM with a public IPv4 address and a sudo user, running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13. Launch Your First EC2 Instance walks through one provider; any provider works.
- Access to the provider's browser or serial console for that VM. You'll change firewall rules over SSH, and the console is how you get back in if a rule cuts off your session.
- Access to the provider's firewall or security group. It's separate from the firewall on the VM, and both have to allow the VPN port.
- An OpenVPN client on each device that will connect.
Which OpenVPN version you'll get
The packaged version depends on the release. As of October 2026, Ubuntu's package archive lists OpenVPN 2.6 for 24.04 and 2.7 for 26.04, and Debian's lists 2.6 for Debian 12 and 13 (Debian 13 also has 2.7 in trixie-backports). Ubuntu 22.04 ships 2.5, which this guide doesn't cover.
The configuration below sets topology subnet and dh none explicitly rather than relying on version-specific defaults. Check the reference manual shipped with your installed package using man openvpn before adapting the configuration to a different release.
Directives are documented in the OpenVPN 2.6 reference manual, certificate commands in the Easy-RSA documentation, and Ubuntu's OpenVPN how-to covers the same packages.
An earlier version of this page started with the kylemanna/openvpn Docker image. Its GitHub repository has had no commits since December 2020, so it doesn't follow current OpenVPN or OpenSSL releases. The distribution packages used here get security updates through apt.
Install the packages
sudo apt update
sudo apt install openvpn easy-rsa
openvpn --version | head -n 1
Check: the last command prints a line that starts with OpenVPN 2.6 or OpenVPN 2.7. Keep that number in mind when you read the version notes above.
Create the certificate authority
OpenVPN authenticates the server and every client with certificates signed by a certificate authority (CA) that you run yourself. Easy-RSA manages it: it creates keys, signs requests and revokes certificates. The Easy-RSA docs recommend running it as a normal user, so keep the CA in your home directory rather than in /etc/openvpn. The CA key never goes to clients.
vars file.make-cadir ~/openvpn-ca
cd ~/openvpn-ca
nano vars
Add these two lines to vars to use elliptic-curve keys and a SHA-512 signature digest:
set_var EASYRSA_ALGO ec
set_var EASYRSA_DIGEST "sha512"
With elliptic-curve keys the server doesn't need classic Diffie-Hellman parameters, which is why the config later says dh none and there's no gen-dh step. The organisation fields in vars are optional: Easy-RSA's default cn_only subject format is enough for a VPN.
Initialise the PKI and build the CA. Easy-RSA asks for a passphrase for the CA key (you'll need it every time you sign or revoke a certificate) and a Common Name, which can be anything descriptive:
./easyrsa init-pki
./easyrsa build-ca
Check: openssl x509 -noout -subject -enddate -in pki/ca.crt shows the name you chose and the CA's expiry date.
Back up the CA now
pki/private/ca.key signs every certificate. If you lose it, you rebuild the PKI and hand every device a new profile; if it leaks, anyone can create client certificates. Archive the directory and copy it off the server to encrypted storage:
umask 077
BACKUP="$HOME/openvpn-ca-$(date +%F).tar.gz"
install -m 600 /dev/null "$BACKUP"
tar -czf "$BACKUP" -C ~ openvpn-ca
# then, from your own computer:
# scp USER@SERVER:openvpn-ca-*.tar.gz .
Repeat the backup after you issue or revoke certificates, because pki/index.txt records which ones exist.
Issue the server certificate and tls-crypt key
./easyrsa gen-req server nopass
./easyrsa sign-req server server
openvpn --genkey tls-crypt tc.key
sign-req asks you to type yes and then the CA passphrase. nopass leaves the server key unencrypted so the service can start at boot without a prompt; file permissions protect it instead.
The last command creates a shared key for tls-crypt, which encrypts and authenticates every control-channel packet. The server drops packets that weren't made with this key without answering, so port scans and junk traffic get nothing back, and the certificates exchanged during the handshake stay hidden. Unlike the older tls-auth, the manual notes that tls-crypt doesn't need a key-direction setting. Ubuntu's how-to uses tls-crypt as well.
Copy the server's files into place and lock down the two secrets:
sudo cp pki/ca.crt pki/issued/server.crt pki/private/server.key tc.key /etc/openvpn/server/
sudo chmod 600 /etc/openvpn/server/server.key /etc/openvpn/server/tc.key
sudo ls -l /etc/openvpn/server/
Check: the listing shows four files, with server.key and tc.key readable only by root (-rw-------).
Write the server config
sudo nano /etc/openvpn/server/server.conf
port 1194
proto udp
dev tun0
ca ca.crt
cert server.crt
key server.key
dh none
tls-crypt tc.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 9.9.9.9"
keepalive 10 120
user nobody
group nogroup
persist-key
persist-tun
explicit-exit-notify 1
verb 3
server 10.8.0.0 255.255.255.0hands clients addresses from 10.8.0.0/24, with the server at 10.8.0.1. Choose a range that doesn't overlap the networks your clients sit on.topology subnetgives each client one address in that /24. OpenVPN 2.6 still defaults to the oldernet30layout for servers; 2.7 switched tosubnet.redirect-gateway def1sends all of a client's IPv4 traffic through the tunnel. For a split tunnel, delete that line and push only the networks you need, for examplepush "route 172.31.0.0 255.255.0.0". This tunnel carries IPv4 only; the manual'sserver-ipv6directive adds IPv6 if you need it.- There's no
cipherline on purpose. The manual says data-channel ciphers are negotiated fromdata-ciphers, which defaults to AES-256-GCM and AES-128-GCM, plus CHACHA20-POLY1305 where the TLS library supports it. user nobodyandgroup nogroupdrop root privileges after start-up;persist-keyandpersist-tunlet the process restart without needing them back.explicit-exit-notify 1tells UDP clients when the server restarts, so they reconnect straight away instead of waiting for a timeout. It only works with UDP.- No log file is set, so output goes to the systemd journal.
Turn on IP forwarding
Without forwarding, clients connect but can't reach anything beyond the server.
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-openvpn.conf
sudo sysctl --system
cat /proc/sys/net/ipv4/ip_forward
Check: the last command prints 1. UFW applies its own /etc/ufw/sysctl.conf, and ufw-framework(8) says that file overrides the system settings, so make sure it doesn't set forwarding back to 0.
Open the firewall and add NAT
Before you change the firewall: open the provider's console for the VM in another browser tab and confirm you can log in there. If UFW isn't enabled yet, ufw enable switches on a deny-incoming policy, and SSH has to be allowed before that happens or your session is cut off.
Find the public interface name (the word after dev) and check UFW's current state. This guide writes eth0; use your real name everywhere below:
ip route show default
sudo ufw status verbose
UFW drops routed traffic by default and doesn't translate addresses on its own. Following the IP masquerading example in ufw-framework(8), add this block at the end of /etc/ufw/before.rules, after the COMMIT line of the *filter section:
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
Now add the rules. SSH comes first. If your SSH server listens on a port other than 22, allow that port instead of the OpenSSH profile (Locking Down SSH covers the safe order for SSH changes):
sudo ufw allow OpenSSH
sudo ufw allow 1194/udp
sudo ufw route allow in on tun0 out on eth0 from 10.8.0.0/24
sudo ufw show added
sudo ufw enable
# if UFW was already enabled, reload it so it reads before.rules
sudo ufw reload
Review the output of ufw show added before you run ufw enable: it should list the SSH rule. Then allow UDP 1194 in the provider's firewall or security group too.
Check: open a second SSH session to the VM before closing the first. sudo ufw status verbose should show the SSH and 1194/udp rules and an ALLOW FWD line from tun0, and sudo iptables -t nat -S POSTROUTING should show the MASQUERADE rule on your interface.
Start the server
sudo systemctl enable --now openvpn-server@server
sudo journalctl -u openvpn-server@server -b --no-pager | tail -n 20
ip addr show dev tun0
The openvpn-server@server unit reads /etc/openvpn/server/server.conf and runs in that directory, which is why the config can use bare file names.
Check: the journal ends with Initialization Sequence Completed, and tun0 has the address 10.8.0.1/24. If the service stopped instead, the journal names the line in server.conf it didn't accept.
Build a profile for each device
Give every device its own certificate. Two devices sharing one certificate knock each other off: the server log shows MULTI: new connection by client ... will cause previous active sessions by this client to be dropped. Separate certificates also mean you can revoke a lost phone without touching anything else.
cd ~/openvpn-ca
./easyrsa gen-req laptop nopass
./easyrsa sign-req client laptop
Leave out nopass if you want the client key protected by a passphrase that the user types when connecting.
Create a template for the client settings once, as ~/client-base.conf. Replace vpn.example.com with the server's public IP address or a DNS name that points to it:
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verb 3
remote-cert-tls server makes the client check that the certificate it receives was issued for a server, which the manual describes as protection against one client impersonating the server to another. Then assemble a single .ovpn file with the certificates and keys inline:
cd ~/openvpn-ca
NAME=laptop
{
cat ~/client-base.conf
echo "<ca>"; cat pki/ca.crt; echo "</ca>"
echo "<cert>"; openssl x509 -in pki/issued/$NAME.crt; echo "</cert>"
echo "<key>"; cat pki/private/$NAME.key; echo "</key>"
echo "<tls-crypt>"; cat tc.key; echo "</tls-crypt>"
} > ~/$NAME.ovpn
chmod 600 ~/$NAME.ovpn
openssl x509 writes only the PEM block of the certificate, without the text summary Easy-RSA puts in front of it. The file holds the device's private key and the shared tls-crypt key, so move it over a secure channel (for example scp), and delete copies you no longer need, including the one in your home directory.
Connect the clients
- Windows, macOS, iOS and Android: install OpenVPN Connect and import the
.ovpnfile. - Linux desktop: with the NetworkManager OpenVPN plugin installed (
network-manager-openvpnon Debian and Ubuntu), runnmcli connection import type openvpn file laptop.ovpnor import it in the network settings. NetworkManager applies the pushed DNS servers. - Linux command line:
sudo openvpn --config laptop.ovpnbrings the tunnel up, but on its own it doesn't apply the pusheddhcp-option DNSservers. Use NetworkManager if you want DNS handled for you.
Check: on the server, sudo journalctl -u openvpn-server@server -f shows Peer Connection Initiated with the client's name while it connects, and sudo cat /run/openvpn-server/status-server.log lists connected clients and their tunnel addresses. On the client, ping 10.8.0.1 should answer, and with the full tunnel curl https://ifconfig.me should print the server's public IP.
Two log lines look like warnings but aren't errors: Kernel support for ovpn-dco missing, disabling data channel offload just means the kernel module for in-kernel data handling isn't loaded, and a client note that --cipher is not set refers to a fallback for OpenVPN versions older than 2.5. Ubuntu's how-to shows both in a working connection.
Add a TCP 443 instance for restrictive networks
Rather than switching the server to TCP, run a second instance next to the UDP one. Clients try UDP first and fall back to TCP when UDP doesn't get through. Expect lower throughput and higher latency over TCP: carrying TCP inside TCP means both layers retransmit lost packets. Port 443 must be free on the VM, so this doesn't work as written on a server that already runs a web server on 443.
Create /etc/openvpn/server/server-tcp.conf. It reuses the same certificates but needs its own device and address range:
port 443
proto tcp
dev tun1
ca ca.crt
cert server.crt
key server.key
dh none
tls-crypt tc.key
topology subnet
server 10.9.0.0 255.255.255.0
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
user nobody
group nogroup
persist-key
persist-tun
verb 3
There's no explicit-exit-notify line because that option is for UDP only. Add a second MASQUERADE line to the *nat block in /etc/ufw/before.rules, directly under the first one:
-A POSTROUTING -s 10.9.0.0/24 -o eth0 -j MASQUERADE
Then open the port, allow forwarding from the new device and start the instance:
sudo ufw allow 443/tcp
sudo ufw route allow in on tun1 out on eth0 from 10.9.0.0/24
sudo ufw reload
sudo systemctl enable --now openvpn-server@server-tcp
Allow TCP 443 in the provider's firewall as well. In ~/client-base.conf, delete the proto udp line and replace the single remote line with two; the manual says the client tries remote entries in the order they're listed:
remote vpn.example.com 1194 udp
remote vpn.example.com 443 tcp
Rebuild and redistribute the .ovpn files. Check: sudo ss -tlnp | grep ':443' shows OpenVPN listening, and a client on a network that blocks UDP connects with a TCP line in its log.
Revoke a device
When a device is lost or someone leaves, revoke the certificate and publish a certificate revocation list (CRL):
cd ~/openvpn-ca
./easyrsa revoke laptop
./easyrsa gen-crl
sudo install -m 644 pki/crl.pem /etc/openvpn/server/crl.pem
The first time, add this line to server.conf (and server-tcp.conf if you use it), then restart the instances:
crl-verify crl.pem
sudo systemctl restart openvpn-server@server
sudo systemctl restart openvpn-server@server-tcp # only if you added it
The file is world-readable because OpenVPN reads it after dropping to nobody, and it doesn't contain secrets. Restarting also disconnects the revoked device straight away; otherwise a connected client keeps its session until the next TLS renegotiation, which happens hourly by default (reneg-sec 3600).
A CRL has an expiry date of its own. Once it passes, the server refuses every client, not just revoked ones. Check the date with openssl crl -noout -nextupdate -in pki/crl.pem, and run gen-crl and the install command again before then.
Troubleshooting
Keep the log open while a client tries to connect:
sudo journalctl -u openvpn-server@server -f
Nothing appears in the log
The packets aren't reaching OpenVPN. Check the provider's firewall rule for UDP 1194, sudo ufw status verbose on the VM, and that the server listens with sudo ss -ulnp | grep 1194 (ss -tlnp for the TCP instance). Also check that the client's remote line holds the public address, not the VM's private one.
Errors that mention tls-crypt or packet authentication
The client's <tls-crypt> block doesn't match /etc/openvpn/server/tc.key, usually because the profile was built before the key was regenerated. Rebuild the profile from the current tc.key.
VERIFY ERROR or certificate errors
The client's CA certificate doesn't match the server's, a certificate has expired, or it has been revoked. In ~/openvpn-ca, openssl x509 -noout -enddate -in pki/issued/laptop.crt shows a certificate's expiry, and pki/index.txt marks revoked certificates with R. If every client fails at once after you set up revocation, check whether the CRL has expired.
Two devices keep dropping each other
They share one certificate; see the MULTI: new connection message above. Issue a separate certificate and profile for each.
Connected, but no internet
- Forwarding:
cat /proc/sys/net/ipv4/ip_forwardmust print1. - NAT:
sudo iptables -t nat -S POSTROUTINGmust show the MASQUERADE rule for 10.8.0.0/24 on the interface fromip route show default. If you editedbefore.ruleswhile UFW was running, runsudo ufw reload. - Routed traffic:
sudo ufw status verbosemust list theALLOW FWDrule fromtun0to the public interface. - DNS: if
ping 1.1.1.1works through the tunnel but names don't resolve, the client isn't applying the pushed DNS servers (see the Linux command-line note above). Networking Fundamentals has a step-by-step way to separate routing problems from DNS problems.
Removing the server
If you set this up to try it out, undo it in this order so nothing stays open:
sudo systemctl disable --now openvpn-server@server openvpn-server@server-tcp
sudo ufw delete allow 1194/udp
sudo ufw delete allow 443/tcp
sudo ufw route delete allow in on tun0 out on eth0 from 10.8.0.0/24
sudo ufw route delete allow in on tun1 out on eth0 from 10.9.0.0/24
# remove the *nat block you added to /etc/ufw/before.rules, then:
sudo ufw reload
sudo rm /etc/sysctl.d/99-openvpn.conf
sudo apt purge openvpn easy-rsa
Skip the lines for the TCP instance if you didn't create it. Remove the VPN ports from the provider's firewall, and delete the VM if it existed only for this, so it stops costing money. Decide what happens to ~/openvpn-ca and its backups: keep them in encrypted storage if you might reuse the CA, or delete them so the CA key can't turn up later. Remove the profiles from the client devices too.
Where to go next
- Lock down SSH on the same VM with Locking Down SSH; the VPN server is still an internet-facing machine.
- Compare the setup with WireGuard Cloud Deployment if your clients don't need TCP.
- If managing certificates by hand becomes a chore, Pritunl runs OpenVPN behind a web console with users and organizations.



