VPN 📖 16 min read

OpenVPN Cloud Deployment

This guide builds an OpenVPN server on an Ubuntu or Debian cloud VM from the distribution packages: an Easy-RSA certificate authority, a server config that behaves the same on OpenVPN 2.6 and 2.7, forwarding and NAT with UFW, one profile file per device, a second instance on TCP 443 for networks that block UDP, and certificate revocation. Each step ends with a command that shows whether it worked.

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-crypt makes 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

Terminal window with the commands from the section Install the packages
Install OpenVPN and Easy-RSA, then confirm the version.
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.

Terminal window with the commands from this section
Create the certificate authority directory, then edit its 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.0 hands 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 subnet gives each client one address in that /24. OpenVPN 2.6 still defaults to the older net30 layout for servers; 2.7 switched to subnet.
  • redirect-gateway def1 sends 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 example push "route 172.31.0.0 255.255.0.0". This tunnel carries IPv4 only; the manual's server-ipv6 directive adds IPv6 if you need it.
  • There's no cipher line on purpose. The manual says data-channel ciphers are negotiated from data-ciphers, which defaults to AES-256-GCM and AES-128-GCM, plus CHACHA20-POLY1305 where the TLS library supports it.
  • user nobody and group nogroup drop root privileges after start-up; persist-key and persist-tun let the process restart without needing them back.
  • explicit-exit-notify 1 tells 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.

Terminal window with the commands from the section Open the firewall and add NAT
Allow SSH before enabling the firewall, open 1194/udp, and route VPN traffic out through eth0.

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 .ovpn file.
  • Linux desktop: with the NetworkManager OpenVPN plugin installed (network-manager-openvpn on Debian and Ubuntu), run nmcli connection import type openvpn file laptop.ovpn or import it in the network settings. NetworkManager applies the pushed DNS servers.
  • Linux command line: sudo openvpn --config laptop.ovpn brings the tunnel up, but on its own it doesn't apply the pushed dhcp-option DNS servers. 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.

Terminal window with the commands from the section Add a TCP 443 instance for restrictive networks
A second instance on TCP 443, for networks that block UDP.

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_forward must print 1.
  • NAT: sudo iptables -t nat -S POSTROUTING must show the MASQUERADE rule for 10.8.0.0/24 on the interface from ip route show default. If you edited before.rules while UFW was running, run sudo ufw reload.
  • Routed traffic: sudo ufw status verbose must list the ALLOW FWD rule from tun0 to the public interface.
  • DNS: if ping 1.1.1.1 works 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.