{"id":166229,"date":"2026-04-15T00:19:29","date_gmt":"2026-04-14T21:19:29","guid":{"rendered":"https:\/\/computingforgeeks.com\/install-openvpn-ubuntu-2604\/"},"modified":"2026-06-09T02:10:04","modified_gmt":"2026-06-08T23:10:04","slug":"install-openvpn-server-ubuntu","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/install-openvpn-server-ubuntu\/","title":{"rendered":"Install OpenVPN Server on Ubuntu 26.04, 24.04 &#038; 22.04"},"content":{"rendered":"<p>You want a VPN that lives on your own box, terminates where you decide, and logs exactly what you tell it to log. An OpenVPN server is still the most flexible way to do that on Ubuntu: certificate authentication, a TLS-crypt control channel, an AES-256-GCM data channel, and a single <code>.ovpn<\/code> file you hand to a laptop or a phone. It runs the same way on the three Ubuntu LTS releases people actually have in production right now.<\/p>\n\n<p>This guide builds a full deployment on Ubuntu Server and tests it end to end. We construct the PKI with Easy-RSA, write the server config, wire UFW with a NAT rule for the <code>10.8.0.0\/24<\/code> pool, push DNS to clients, then connect real machines back over <code>tun0<\/code>: an Ubuntu client, a Windows client through the OpenVPN GUI, and notes for macOS, Android, and iOS. You also get certificate-based onboarding for extra users, CRL revocation, and the log commands that tell you why a client will not connect.<\/p>\n\n<p><em>Tested June 2026 on Ubuntu 26.04, 24.04, and 22.04 (OpenVPN 2.7.0, 2.6.19, and 2.5.11). Every command below was run on real servers and verified with a connected client on each release.<\/em><\/p>\n\n<h2>What you need before you start<\/h2>\n\n<p>One Ubuntu server to host the VPN, with a public, routable IP that clients reach on UDP 1194. Any of the three current LTS releases works: 26.04 (Resolute Raccoon), 24.04 (Noble Numbat), or 22.04 (Jammy Jellyfish). You also want at least one machine to test from. Throughout the article the public endpoint is shown as <code>203.0.113.20<\/code> (RFC 5737 documentation range). Swap in your own public IP or DNS name wherever you see it.<\/p>\n\n<ul>\n  <li>An Ubuntu 26.04, 24.04, or 22.04 server with root or sudo access<\/li>\n  <li>A public IPv4 on the server, with UDP 1194 reachable from the internet<\/li>\n  <li>UFW available (installed by default on the server images)<\/li>\n  <li>At least one client device: Linux, Windows, macOS, Android, or iOS<\/li>\n  <li>Basic familiarity with systemd units and <code>journalctl<\/code><\/li>\n<\/ul>\n\n<p>If you have not done the baseline yet, run through the <a href=\"https:\/\/computingforgeeks.com\/ubuntu-2604-initial-server-setup\/\">initial server setup<\/a> and the <a href=\"https:\/\/computingforgeeks.com\/harden-ubuntu-2604-server\/\">server hardening guide<\/a> first. Both are worth ten minutes before you expose anything on UDP 1194.<\/p>\n\n<h2>Step 1: Install OpenVPN and Easy-RSA<\/h2>\n\n<p>Both packages ship in the default Ubuntu archive on all three releases. No third-party PPA, no manual build.<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo apt update\nsudo apt install -y openvpn easy-rsa ufw<\/code><\/pre>\n\n<p>Confirm what landed. The version differs by release, which matters for one thing only (data channel offload, covered just below):<\/p>\n\n<pre class=\"wp-block-code code\"><code>openvpn --version | head -1\ndpkg-query -W -f='${Package} ${Version}\\n' openvpn easy-rsa<\/code><\/pre>\n\n<p>Here is the same pair of commands run on each of the three LTS releases, side by side:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1800\" height=\"1200\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu.png\" alt=\"openvpn --version output on Ubuntu 26.04, 24.04 and 22.04 showing OpenVPN 2.7, 2.6 and 2.5\" class=\"wp-image-168613\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu.png 1800w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu-300x200.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu-1024x683.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu-768x512.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-versions-ubuntu-1536x1024.png 1536w\" sizes=\"auto, (max-width: 1800px) 100vw, 1800px\" \/><\/figure>\n\n\n<p>The numbers map cleanly to each release. This is the only place versions matter in the whole build:<\/p>\n\n<table>\n<thead><tr><th>Ubuntu release<\/th><th>OpenVPN<\/th><th>Easy-RSA<\/th><th>Kernel data channel offload (DCO)<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>26.04 LTS (Resolute Raccoon)<\/td><td>2.7.0<\/td><td>3.2.5<\/td><td>Yes, via the in-tree ovpn module<\/td><\/tr>\n<tr><td>24.04 LTS (Noble Numbat)<\/td><td>2.6.19<\/td><td>3.1.7<\/td><td>Yes, via the ovpn-dco module<\/td><\/tr>\n<tr><td>22.04 LTS (Jammy Jellyfish)<\/td><td>2.5.11<\/td><td>3.0.8<\/td><td>No, the data channel runs in userspace<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>DCO (Data Channel Offload) moves the encrypted data path into the kernel instead of copying every packet up to the OpenVPN process. On 24.04 and 26.04 it is on by default and you will see an <code>ovpn-dco<\/code> device come up in the logs. On 22.04 the tunnel works exactly the same, it just runs the data channel in userspace, which costs you some throughput on a busy link. The configuration is identical regardless, so the rest of this guide is one set of commands for all three.<\/p>\n\n<h2>Step 2: Build the PKI with Easy-RSA<\/h2>\n\n<p>OpenVPN is certificate-authenticated. That means a small private CA, one server certificate, one certificate per client, and a TLS-crypt static key that gates the control channel before any TLS handshake starts. Easy-RSA 3 wraps all of it.<\/p>\n\n<p>Create a working copy of Easy-RSA under <code>\/etc\/openvpn\/<\/code> so the scripts run with the right paths:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo mkdir -p \/etc\/openvpn\/easy-rsa\nsudo ln -sf \/usr\/share\/easy-rsa\/* \/etc\/openvpn\/easy-rsa\/\ncd \/etc\/openvpn\/easy-rsa<\/code><\/pre>\n\n<p>Easy-RSA reads its defaults from a <code>vars<\/code> file. Open one for editing:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo nano \/etc\/openvpn\/easy-rsa\/vars<\/code><\/pre>\n\n<p>Add the following. We pick ECDSA on the <code>secp384r1<\/code> curve: it is faster than RSA, the keys are smaller, and every Easy-RSA version from 3.0.8 up supports it, so this block is identical on all three releases.<\/p>\n\n<pre class=\"wp-block-code code\"><code>set_var EASYRSA_ALGO      ec\nset_var EASYRSA_CURVE     secp384r1\nset_var EASYRSA_REQ_COUNTRY    \"US\"\nset_var EASYRSA_REQ_PROVINCE   \"Example\"\nset_var EASYRSA_REQ_CITY       \"ExampleCity\"\nset_var EASYRSA_REQ_ORG        \"computingforgeeks\"\nset_var EASYRSA_REQ_EMAIL      \"admin@example.com\"\nset_var EASYRSA_REQ_OU         \"VPN\"\nset_var EASYRSA_CERT_EXPIRE    3650\nset_var EASYRSA_CA_EXPIRE      3650<\/code><\/pre>\n\n<p>Initialize the PKI and build the CA. The <code>nopass<\/code> flag leaves the CA private key unencrypted on disk, which is fine for a dedicated VPN box. On shared hardware, drop <code>nopass<\/code> and enter a passphrase.<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo .\/easyrsa init-pki\nsudo EASYRSA_BATCH=1 .\/easyrsa --req-cn=\"OpenVPN-CA\" build-ca nopass<\/code><\/pre>\n\n<p>Easy-RSA prints the path to the CA certificate it just wrote:<\/p>\n\n<pre class=\"wp-block-code code\"><code>Notice\n------\nCA creation complete. Your new CA certificate is at:\n* \/etc\/openvpn\/easy-rsa\/pki\/ca.crt\n\nBuild-ca completed successfully.<\/code><\/pre>\n\n<p>Issue the server certificate and the first client certificate, both signed by that CA:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo .\/easyrsa --batch build-server-full server nopass\nsudo .\/easyrsa --batch build-client-full client1 nopass<\/code><\/pre>\n\n<p>Generate the certificate revocation list. Even if you never revoke anything, OpenVPN refuses to start with <code>crl-verify<\/code> pointing at a file that does not exist:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo .\/easyrsa gen-crl<\/code><\/pre>\n\n<p>Finally the TLS-crypt static key. This one key encrypts and authenticates every control-channel packet, so a network observer cannot even see the TLS handshake begin, let alone fingerprint it. The <code>--genkey tls-crypt<\/code> form works on OpenVPN 2.5, 2.6, and 2.7 alike:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo openvpn --genkey tls-crypt \/etc\/openvpn\/easy-rsa\/pki\/tc.key<\/code><\/pre>\n\n<p>Confirm the PKI now holds everything the server and clients need:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo ls \/etc\/openvpn\/easy-rsa\/pki\/issued \/etc\/openvpn\/easy-rsa\/pki\/private<\/code><\/pre>\n\n<p>You should see the server and client certificates alongside their private keys:<\/p>\n\n<pre class=\"wp-block-code code\"><code>\/etc\/openvpn\/easy-rsa\/pki\/issued:\nclient1.crt  server.crt\n\n\/etc\/openvpn\/easy-rsa\/pki\/private:\nca.key  client1.key  server.key<\/code><\/pre>\n\n<p>Running <code>.\/easyrsa show-ca<\/code> prints the subject and validity of the authority every certificate chains back to, a quick sanity check before the server uses it:<\/p>\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-openvpn-easyrsa-status-ubuntu-2604.png\" alt=\"Easy-RSA PKI show-ca output on Ubuntu after building the OpenVPN certificate authority\" title=\"\"><\/figure>\n\n\n<p>With the PKI in place, the server needs only a handful of those files to start.<\/p>\n\n<h2>Step 3: Write the OpenVPN server config<\/h2>\n\n<p>Copy only the files the server process needs into <code>\/etc\/openvpn\/server\/<\/code>. Keeping a clean copy (rather than symlinking into <code>pki\/<\/code>) means you can back up or rotate the PKI without disturbing a running daemon:<\/p>\n\n<pre class=\"wp-block-code code\"><code>cd \/etc\/openvpn\/easy-rsa\/pki\nsudo mkdir -p \/etc\/openvpn\/server \/var\/log\/openvpn\nsudo cp ca.crt issued\/server.crt private\/server.key tc.key crl.pem \/etc\/openvpn\/server\/\nsudo chown nobody:nogroup \/etc\/openvpn\/server\/crl.pem<\/code><\/pre>\n\n<p>The <code>crl.pem<\/code> ownership matters: OpenVPN drops to the <code>nobody<\/code> user after startup and still has to read the CRL on every new connection.<\/p>\n\n<p>Open the server config:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo nano \/etc\/openvpn\/server\/server.conf<\/code><\/pre>\n\n<p>Paste the following. This is a full-tunnel setup on <code>10.8.0.0\/24<\/code> over UDP 1194 with modern ciphers and no legacy fallbacks. Each block maps to one decision: transport, addresses, what to push, authentication, logging.<\/p>\n\n<pre class=\"wp-block-code code\"><code>port 1194\nproto udp4\ndev tun\n\nca   \/etc\/openvpn\/server\/ca.crt\ncert \/etc\/openvpn\/server\/server.crt\nkey  \/etc\/openvpn\/server\/server.key\ndh   none\n\ntopology subnet\nserver 10.8.0.0 255.255.255.0\nifconfig-pool-persist \/var\/log\/openvpn\/ipp.txt\n\npush \"redirect-gateway def1 bypass-dhcp\"\npush \"dhcp-option DNS 1.1.1.1\"\npush \"dhcp-option DNS 9.9.9.9\"\n\nkeepalive 10 120\ntls-crypt \/etc\/openvpn\/server\/tc.key\ncipher AES-256-GCM\nauth SHA256\ndata-ciphers AES-256-GCM:AES-128-GCM\n\nuser nobody\ngroup nogroup\npersist-key\npersist-tun\n\ncrl-verify \/etc\/openvpn\/server\/crl.pem\n\nstatus    \/var\/log\/openvpn\/openvpn-status.log\nlog-append \/var\/log\/openvpn\/openvpn.log\nverb 3\nexplicit-exit-notify 1<\/code><\/pre>\n\n<p>A few directives earn their place. <code>dh none<\/code> is correct because the certificates are ECDSA, so there is no classic Diffie-Hellman exchange. <code>tls-crypt<\/code> both signs and encrypts control packets, which hides the OpenVPN fingerprint on the wire. <code>data-ciphers<\/code> is the negotiated list used by 2.6 and 2.7 clients; <code>cipher<\/code> stays for any older 2.5 client that might connect from 22.04.<\/p>\n\n<h2>Step 4: Enable IP forwarding and UFW NAT<\/h2>\n\n<p>OpenVPN does not route anything by itself. The kernel has to forward packets between <code>tun0<\/code> and the public interface, and UFW has to masquerade them on the way out. Both pieces are required for clients to reach the internet through the tunnel.<\/p>\n\n<p>Turn on IPv4 forwarding persistently:<\/p>\n\n<pre class=\"wp-block-code code\"><code>echo 'net.ipv4.ip_forward=1' | sudo tee \/etc\/sysctl.d\/99-openvpn.conf\nsudo sysctl -p \/etc\/sysctl.d\/99-openvpn.conf<\/code><\/pre>\n\n<p>The reload echoes the new value back:<\/p>\n\n<pre class=\"wp-block-code code\"><code>net.ipv4.ip_forward = 1<\/code><\/pre>\n\n<p>The masquerade rule needs the name of your public interface, which is not always <code>eth0<\/code>. Detect it and store it in a shell variable so the next commands stay copy-paste clean:<\/p>\n\n<pre class=\"wp-block-code code\"><code>PUBLIC_NIC=$(ip route get 1.1.1.1 | awk '{print $5; exit}')\necho \"Public interface: ${PUBLIC_NIC}\"<\/code><\/pre>\n\n<p>Open the UFW before-rules file to add a POSTROUTING masquerade for the VPN subnet:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo nano \/etc\/ufw\/before.rules<\/code><\/pre>\n\n<p>At the very bottom of the file, add this stanza. Leave the literal <code>PUBLIC_NIC<\/code> placeholder in place for now; the next command rewrites it:<\/p>\n\n<pre class=\"wp-block-code code\"><code># START OPENVPN RULES\n*nat\n:POSTROUTING ACCEPT [0:0]\n-A POSTROUTING -s 10.8.0.0\/24 -o PUBLIC_NIC -j MASQUERADE\nCOMMIT\n# END OPENVPN RULES<\/code><\/pre>\n\n<p>Substitute the real interface name from the variable into the file:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i \"s\/PUBLIC_NIC\/${PUBLIC_NIC}\/\" \/etc\/ufw\/before.rules<\/code><\/pre>\n\n<p>Flip the default forward policy so UFW stops dropping traffic that exits <code>tun0<\/code>:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i 's\/DEFAULT_FORWARD_POLICY=\"DROP\"\/DEFAULT_FORWARD_POLICY=\"ACCEPT\"\/' \/etc\/default\/ufw<\/code><\/pre>\n\n<p>Allow SSH and the OpenVPN port, then enable the firewall:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo ufw allow 22\/tcp\nsudo ufw allow 1194\/udp\nsudo ufw --force enable\nsudo ufw status verbose<\/code><\/pre>\n\n<p>UFW should report both ports allowed and the routed forward default:<\/p>\n\n<pre class=\"wp-block-code code\"><code>Status: active\nLogging: on (low)\nDefault: deny (incoming), allow (outgoing), allow (routed)\nNew profiles: skip\n\nTo                         Action      From\n--                         ------      ----\n22\/tcp                     ALLOW IN    Anywhere\n1194\/udp                   ALLOW IN    Anywhere<\/code><\/pre>\n\n<p>For UFW profiles, application groups, and logging tiers in more depth, the <a href=\"https:\/\/computingforgeeks.com\/configure-ufw-firewall-ubuntu-2604\/\">UFW firewall setup<\/a> walks through the rest.<\/p>\n\n<h2>Step 5: Start the OpenVPN server<\/h2>\n\n<p>Ubuntu ships a templated unit at <code>openvpn-server@.service<\/code>. The name after the <code>@<\/code> matches the config filename under <code>\/etc\/openvpn\/server\/<\/code>. Ours is <code>server.conf<\/code>, so the unit is <code>openvpn-server@server<\/code>. This is identical on 22.04, 24.04, and 26.04.<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl enable --now openvpn-server@server\nsudo systemctl status openvpn-server@server --no-pager<\/code><\/pre>\n\n<p>The line you are looking for is <code>Status: \"Initialization Sequence Completed\"<\/code>. It means the certificates loaded, the tun device came up, and the daemon is listening:<\/p>\n\n<pre class=\"wp-block-code code\"><code>\u25cf openvpn-server@server.service - OpenVPN service for server\n     Loaded: loaded (\/usr\/lib\/systemd\/system\/openvpn-server@.service; enabled)\n     Active: active (running) since Mon 2026-06-08 18:00:46 UTC\n   Main PID: 3533 (openvpn)\n     Status: \"Initialization Sequence Completed\"\n      Tasks: 1 (limit: 1499)\n     Memory: 2.0M<\/code><\/pre>\n\n<p>Verify <code>tun0<\/code> carries the first address in the VPN subnet and UDP 1194 is bound:<\/p>\n\n<pre class=\"wp-block-code code\"><code>ip -brief addr show tun0\nsudo ss -lun | grep 1194<\/code><\/pre>\n\n<p>The server side of the tunnel is up at <code>10.8.0.1<\/code> and the listener is open:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1800\" height=\"1200\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu.png\" alt=\"OpenVPN server active on Ubuntu with tun0 10.8.0.1 and client1 in the status log using AES-256-GCM\" class=\"wp-image-168615\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu.png 1800w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu-300x200.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu-1024x683.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu-768x512.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-server-running-ubuntu-1536x1024.png 1536w\" sizes=\"auto, (max-width: 1800px) 100vw, 1800px\" \/><\/figure>\n\n\n<p>With the server listening, the last piece is a profile a client can import.<\/p>\n\n<h2>Step 6: Build the client profile<\/h2>\n\n<p>An inline <code>.ovpn<\/code> profile bundles the CA, the client certificate, 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, where all the key material already lives.<\/p>\n\n<p>First set a variable for the public endpoint your clients will dial. You use this value here and again when you copy the profile, so define it once:<\/p>\n\n<pre class=\"wp-block-code code\"><code>VPN_ENDPOINT=\"203.0.113.20\"<\/code><\/pre>\n\n<p>Create a reusable header file that every client profile starts with. Open it:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo nano \/etc\/openvpn\/client-common.txt<\/code><\/pre>\n\n<p>Paste the client-side directives. The <code>VPN_ENDPOINT<\/code> token is a placeholder that the next command fills in:<\/p>\n\n<pre class=\"wp-block-code code\"><code>client\ndev tun\nproto udp\nremote VPN_ENDPOINT 1194\nresolv-retry infinite\nnobind\npersist-key\npersist-tun\nremote-cert-tls server\ncipher AES-256-GCM\nauth SHA256\ndata-ciphers AES-256-GCM:AES-128-GCM\nverb 3<\/code><\/pre>\n\n<p>Write your real endpoint into the header:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i \"s\/VPN_ENDPOINT\/${VPN_ENDPOINT}\/\" \/etc\/openvpn\/client-common.txt<\/code><\/pre>\n\n<p>Now a small builder script that stitches the header together with one client&#8217;s certificate, key, and the TLS-crypt key. It takes the client name as an argument, so you reuse it for every future user. Open it:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo nano \/etc\/openvpn\/make-client.sh<\/code><\/pre>\n\n<p>Paste the script. There are no here-documents here, just a brace group writing one file, which keeps it copy-paste safe:<\/p>\n\n<pre class=\"wp-block-code code\"><code>#!\/bin\/bash\nPKI=\/etc\/openvpn\/easy-rsa\/pki\nOUT=\/root\nNAME=\"$1\"\n\n{\n  cat \/etc\/openvpn\/client-common.txt\n  echo \"&lt;ca&gt;\";        cat \"${PKI}\/ca.crt\";              echo \"&lt;\/ca&gt;\"\n  echo \"&lt;cert&gt;\";      cat \"${PKI}\/issued\/${NAME}.crt\"; echo \"&lt;\/cert&gt;\"\n  echo \"&lt;key&gt;\";       cat \"${PKI}\/private\/${NAME}.key\"; echo \"&lt;\/key&gt;\"\n  echo \"&lt;tls-crypt&gt;\"; cat \"${PKI}\/tc.key\";             echo \"&lt;\/tls-crypt&gt;\"\n} &gt; \"${OUT}\/${NAME}.ovpn\"\n\necho \"Wrote ${OUT}\/${NAME}.ovpn\"<\/code><\/pre>\n\n<p>Make it executable and run it for the first client:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo chmod +x \/etc\/openvpn\/make-client.sh\nsudo \/etc\/openvpn\/make-client.sh client1<\/code><\/pre>\n\n<p>The finished profile lands at <code>\/root\/client1.ovpn<\/code>. That single file is the only thing you copy to a client device.<\/p>\n\n<h2>Step 7: Connect from an Ubuntu client<\/h2>\n\n<p>On a second Ubuntu machine, install the OpenVPN package:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y openvpn<\/code><\/pre>\n\n<p>Copy the profile from the server, rename it <code>client.conf<\/code>, and drop it under <code>\/etc\/openvpn\/client\/<\/code> so the systemd template unit picks it up:<\/p>\n\n<pre class=\"wp-block-code code\"><code>scp root@203.0.113.20:\/root\/client1.ovpn \/tmp\/client1.ovpn\nsudo install -m 600 \/tmp\/client1.ovpn \/etc\/openvpn\/client\/client.conf<\/code><\/pre>\n\n<p>Start the matching unit:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl enable --now openvpn-client@client\nsudo journalctl -u openvpn-client@client --no-pager | tail -6<\/code><\/pre>\n\n<p>On 24.04 and 26.04 you will see the <code>ovpn-dco<\/code> device open (the kernel offload from Step 1), followed by the line that confirms the handshake finished:<\/p>\n\n<pre class=\"wp-block-code code\"><code>openvpn[2345]: ovpn-dco device [tun0] opened\nopenvpn[2345]: net_iface_up: set tun0 up\nopenvpn[2345]: net_addr_v4_add: 10.8.0.2\/24 dev tun0\nopenvpn[2345]: Initialization Sequence Completed<\/code><\/pre>\n\n<p>Now prove the tunnel actually carries traffic. The client takes the next free address from the pool, pings the server end, and points its default route at the tunnel:<\/p>\n\n<pre class=\"wp-block-code code\"><code>ip -brief addr show tun0\nping -c 3 10.8.0.1\nip route get 1.1.1.1<\/code><\/pre>\n\n<p>The replies confirm encryption and routing end to end, and the route lookup shows internet-bound traffic leaving through <code>10.8.0.1<\/code> rather than the local gateway:<\/p>\n\n<pre class=\"wp-block-code code\"><code>tun0             UP             10.8.0.2\/24 fe80::7c4c:ff3f:5a7d:2e95\/64\nPING 10.8.0.1 (10.8.0.1) 56(84) bytes of data.\n64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=0.487 ms\n64 bytes from 10.8.0.1: icmp_seq=2 ttl=64 time=0.527 ms\n64 bytes from 10.8.0.1: icmp_seq=3 ttl=64 time=0.562 ms\n1.1.1.1 via 10.8.0.1 dev tun0 src 10.8.0.2<\/code><\/pre>\n\n<p>The same client mid-session looks like this, with the tunnel address, the ping replies, and the default route all pointing through <code>10.8.0.1<\/code>:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1800\" height=\"1200\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu.png\" alt=\"Ubuntu OpenVPN client on tun0 10.8.0.2 with default route via 10.8.0.1 through the VPN\" class=\"wp-image-168614\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu.png 1800w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu-300x200.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu-1024x683.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu-768x512.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-client-tunnel-ubuntu-1536x1024.png 1536w\" sizes=\"auto, (max-width: 1800px) 100vw, 1800px\" \/><\/figure>\n\n\n<p>Back on the server, the live status log names every connected peer with its certificate common name, real address, virtual address, and the negotiated data cipher:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo grep CLIENT_LIST \/var\/log\/openvpn\/openvpn-status.log<\/code><\/pre>\n\n<p>The connected client shows up with its AES-256-GCM data channel:<\/p>\n\n<pre class=\"wp-block-code code\"><code>CLIENT_LIST,client1,203.0.113.55:52539,10.8.0.2,3978,4149,2026-06-08 18:02:20,AES-256-GCM<\/code><\/pre>\n\n<p>That confirms the Linux side end to end. The same server accepts a Windows client with no changes at all.<\/p>\n\n<h2>Connect from Windows with the OpenVPN GUI<\/h2>\n\n<p>Windows has no built-in OpenVPN client, so install the official one. Download the community Windows installer from the <a href=\"https:\/\/openvpn.net\/community-downloads\/\" target=\"_blank\" rel=\"noreferrer noopener\">OpenVPN downloads page<\/a> and run it. The installer bundles the OpenVPN service and the OpenVPN GUI, a small tray application that imports <code>.ovpn<\/code> profiles and manages connections.<\/p>\n\n<p>Copy <code>client1.ovpn<\/code> to the Windows machine (an SFTP client such as WinSCP, or a USB stick for an air-gapped box). Then right-click the OpenVPN GUI icon in the system tray and choose <strong>Import file<\/strong>, or drop the profile into <code>C:\\Users\\YourName\\OpenVPN\\config\\<\/code>. Right-click the tray icon again and click <strong>Connect<\/strong>.<\/p>\n\n<p>The GUI shows the handshake scrolling past, then the tray icon turns green and a balloon reports the assigned <code>10.8.0.x<\/code> address. Confirm it from a Command Prompt:<\/p>\n\n<pre class=\"wp-block-code code\"><code>ipconfig\nping 10.8.0.1<\/code><\/pre>\n\n<p>OpenVPN 2.7 on Windows installs the same kernel Data Channel Offload driver as Linux, so the tunnel shows up as the <strong>OpenVPN Data Channel Offload<\/strong> adapter holding <code>10.8.0.2<\/code>, and the server answers on <code>10.8.0.1<\/code>:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1724\" height=\"1362\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected.png\" alt=\"Windows Command Prompt showing the OpenVPN Data Channel Offload adapter at 10.8.0.2 and ping replies from 10.8.0.1 over the tunnel\" class=\"wp-image-168621\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected.png 1724w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected-300x237.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected-1024x809.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected-768x607.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-openvpn-windows-connected-1536x1213.png 1536w\" sizes=\"auto, (max-width: 1724px) 100vw, 1724px\" \/><\/figure>\n\n\n<p>One Windows note worth knowing: the OpenVPN GUI needs the right to add routes, so if the tunnel connects but traffic does not flow, right-click the GUI shortcut and set it to <strong>Run as administrator<\/strong>, or let the bundled OpenVPN interactive service handle routing instead.<\/p>\n\n<h2>Connect from macOS, Android, and iOS<\/h2>\n\n<p>The same <code>client1.ovpn<\/code> profile works on every other platform. Generate a separate certificate per device in production (covered next), but for a quick test you can import the same file.<\/p>\n\n<ul>\n  <li><strong>macOS:<\/strong> Tunnelblick (free, open source) is the common choice. Install it, double-click <code>client1.ovpn<\/code> to import, then connect from the menu-bar icon. The official OpenVPN Connect client works too.<\/li>\n  <li><strong>Android:<\/strong> install <strong>OpenVPN Connect<\/strong> from the Play Store, tap <strong>Import Profile<\/strong>, point it at the <code>.ovpn<\/code> file, and toggle the connection on. Android shows a key icon in the status bar while the tunnel is up.<\/li>\n  <li><strong>iOS and iPadOS:<\/strong> install <strong>OpenVPN Connect<\/strong> from the App Store, then share the <code>.ovpn<\/code> file to it (AirDrop, Files, or email) and tap <strong>Add<\/strong>. iOS prompts once to allow the VPN configuration.<\/li>\n<\/ul>\n\n<p>On mobile, send the profile over a channel you trust. The file contains the client private key, so treat it like a password.<\/p>\n\n<h2>Adding more clients<\/h2>\n\n<p>You never rebuild the CA to onboard a user. Sign a new certificate against the existing PKI and render its profile with the same builder script from Step 6:<\/p>\n\n<pre class=\"wp-block-code code\"><code>cd \/etc\/openvpn\/easy-rsa\nsudo .\/easyrsa --batch build-client-full alice nopass\nsudo \/etc\/openvpn\/make-client.sh alice<\/code><\/pre>\n\n<p>Hand the resulting <code>alice.ovpn<\/code> to the user over a channel you trust, such as a password manager or a short-lived file share. The server needs no restart; it validates each client certificate against the CA and the current CRL at connect time.<\/p>\n\n<h2>Revoking a client<\/h2>\n\n<p>When a laptop is lost or a contract ends, revoke the certificate and refresh the CRL. The server reads the updated list on the next connection attempt:<\/p>\n\n<pre class=\"wp-block-code code\"><code>cd \/etc\/openvpn\/easy-rsa\nsudo .\/easyrsa --batch revoke alice\nsudo .\/easyrsa gen-crl\nsudo cp pki\/crl.pem \/etc\/openvpn\/server\/crl.pem\nsudo chown nobody:nogroup \/etc\/openvpn\/server\/crl.pem\nsudo systemctl restart openvpn-server@server<\/code><\/pre>\n\n<p>Easy-RSA confirms the revocation and reminds you to publish the refreshed CRL:<\/p>\n\n<pre class=\"wp-block-code code\"><code>Revocation was successful. You must run 'gen-crl' and upload\na new CRL to your infrastructure in order to prevent the revoked\ncertificate from being accepted.<\/code><\/pre>\n\n<p>The CRL carries its own expiry (180 days by default on Easy-RSA 3.1 and 3.2). Set a reminder to regenerate it before that window closes, or the server starts rejecting every new connection.<\/p>\n\n<h2>One-command alternative: the openvpn-install script<\/h2>\n\n<p>If you want a working tunnel in two minutes and do not need to understand every directive, the community <code>openvpn-install<\/code> script automates the whole PKI-and-config flow this guide does by hand. It is well-maintained and uses the same Easy-RSA underneath.<\/p>\n\n<pre class=\"wp-block-code code\"><code>wget https:\/\/raw.githubusercontent.com\/angristan\/openvpn-install\/master\/openvpn-install.sh -O openvpn-install.sh\nsudo bash openvpn-install.sh<\/code><\/pre>\n\n<p>The script asks for the port, protocol, DNS resolver, and first client name, then drops a ready <code>.ovpn<\/code> in your home directory. Run it again later to add or revoke clients. The manual build above is still worth doing once, because when something breaks at 2 a.m. you want to know which file holds which key.<\/p>\n\n<h2>Logs and troubleshooting<\/h2>\n\n<p>OpenVPN writes to <code>\/var\/log\/openvpn\/openvpn.log<\/code> (from the <code>log-append<\/code> directive) and also to the systemd journal through the unit. When something breaks, check both.<\/p>\n\n<h3>The client hangs on &#8220;TLS handshake failed&#8221;<\/h3>\n\n<p>This almost always means UDP 1194 is not reaching the server. Watch the public interface with <code>tcpdump<\/code> while the client tries to connect:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sudo tcpdump -ni \"${PUBLIC_NIC}\" udp port 1194<\/code><\/pre>\n\n<p>No packets at all means a cloud security group or an upstream firewall is dropping them before they arrive. Packets arriving but the client still timing out means UFW is blocking them (recheck Step 4) or the process is not bound (recheck <code>sudo ss -lun | grep 1194<\/code>).<\/p>\n\n<h3>Error: &#8220;VERIFY ERROR: depth=0, error=CRL has expired&#8221;<\/h3>\n\n<p>The CRL aged past its validity window. Regenerate it and copy it back into place:<\/p>\n\n<pre class=\"wp-block-code code\"><code>cd \/etc\/openvpn\/easy-rsa\nsudo .\/easyrsa gen-crl\nsudo cp pki\/crl.pem \/etc\/openvpn\/server\/crl.pem\nsudo chown nobody:nogroup \/etc\/openvpn\/server\/crl.pem\nsudo systemctl restart openvpn-server@server<\/code><\/pre>\n\n<p>The server accepts new connections again as soon as the refreshed CRL is in place.<\/p>\n\n<h3>Clients connect but cannot reach the internet<\/h3>\n\n<p>The tunnel is up but NAT is not. Confirm forwarding is on and the masquerade rule loaded:<\/p>\n\n<pre class=\"wp-block-code code\"><code>sysctl net.ipv4.ip_forward\nsudo iptables -t nat -L POSTROUTING -n -v<\/code><\/pre>\n\n<p>You want <code>net.ipv4.ip_forward = 1<\/code> and a MASQUERADE line with source <code>10.8.0.0\/24<\/code> leaving your public interface. If the rule is missing, UFW never loaded the <code>before.rules<\/code> stanza; run <code>sudo ufw disable<\/code> then <code>sudo ufw enable<\/code> to reload it.<\/p>\n\n<h3>Throughput is lower on Ubuntu 22.04<\/h3>\n\n<p>This is expected, not a misconfiguration. OpenVPN 2.5 on 22.04 has no kernel data channel offload, so encryption runs in userspace. If a single client needs maximum throughput, run the server on 24.04 or 26.04 where DCO is active, or move to WireGuard for that workload.<\/p>\n\n<h2>How OpenVPN compares to WireGuard on Ubuntu<\/h2>\n\n<p>OpenVPN is not the only option on modern Ubuntu. If you have not already, read the <a href=\"https:\/\/computingforgeeks.com\/install-wireguard-ubuntu-2604\/\">WireGuard guide<\/a>. The short version, from running both in production:<\/p>\n\n<table>\n<thead><tr><th>Concern<\/th><th>OpenVPN<\/th><th>WireGuard<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Config model<\/td><td>Certificates, CA, and CRL<\/td><td>Static public\/private keys per peer<\/td><\/tr>\n<tr><td>Default port<\/td><td>UDP 1194 (configurable)<\/td><td>UDP 51820 (configurable)<\/td><\/tr>\n<tr><td>Handshake visibility on the wire<\/td><td>Hidden behind tls-crypt<\/td><td>Silent, no reply to unsolicited packets<\/td><\/tr>\n<tr><td>Throughput on a small VM<\/td><td>Good; DCO on 24.04\/26.04 closes the gap<\/td><td>Excellent, kernel-level<\/td><\/tr>\n<tr><td>Client availability<\/td><td>Desktop, mobile, routers, everywhere<\/td><td>Native on recent kernels, apps on all platforms<\/td><\/tr>\n<tr><td>Per-user push (DNS, routes)<\/td><td>Rich, via server directives<\/td><td>Minimal, handled in PostUp scripts<\/td><\/tr>\n<tr><td>Revocation<\/td><td>CRL-based, reversible through the PKI<\/td><td>Delete the peer and reload<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>For a small personal tunnel between machines you control, WireGuard is faster to stand up and faster on the wire; the <a href=\"https:\/\/computingforgeeks.com\/install-tailscale-ubuntu-2604\/\">Tailscale mesh<\/a> is easier still. When you need per-user authentication, centrally managed certificates, granular pushes, or a legacy client, OpenVPN is the one that does the job. A web-managed middle ground like <a href=\"https:\/\/computingforgeeks.com\/install-pritunl-vpn-ubuntu\/\">Pritunl<\/a> sits on top of OpenVPN if you want a dashboard instead of a CLI.<\/p>\n\n<h2>Which client app to use on each platform<\/h2>\n\n<p>The server you just built speaks standard OpenVPN, so any of these clients connect to it with the same <code>.ovpn<\/code> profile. Pick the one that matches each device and you are done:<\/p>\n\n<table>\n<thead><tr><th>Platform<\/th><th>Recommended client<\/th><th>How it imports the profile<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Ubuntu \/ Linux<\/td><td><code>openvpn<\/code> package + systemd unit<\/td><td>Drop the profile in <code>\/etc\/openvpn\/client\/<\/code><\/td><\/tr>\n<tr><td>Windows<\/td><td>OpenVPN GUI (community installer)<\/td><td>Tray icon, Import file<\/td><\/tr>\n<tr><td>macOS<\/td><td>Tunnelblick or OpenVPN Connect<\/td><td>Double-click the <code>.ovpn<\/code><\/td><\/tr>\n<tr><td>Android<\/td><td>OpenVPN Connect<\/td><td>Import Profile, pick the file<\/td><\/tr>\n<tr><td>iOS \/ iPadOS<\/td><td>OpenVPN Connect<\/td><td>Share the file to the app, Add<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>Keep the CA offline once it is built, generate one certificate per device rather than sharing a single profile, and rotate the TLS-crypt key every year or two. That gives you a self-hosted VPN you can actually trust, on whichever Ubuntu LTS your server happens to run.<\/p>","protected":false},"excerpt":{"rendered":"<p>You want a VPN that lives on your own box, terminates where you decide, and logs exactly what you tell it to log. An OpenVPN server is still the most flexible way to do that on Ubuntu: certificate authentication, a TLS-crypt control channel, an AES-256-GCM data channel, and a single .ovpn file you hand to &#8230; <a title=\"Install OpenVPN Server on Ubuntu 26.04, 24.04 &#038; 22.04\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/install-openvpn-server-ubuntu\/\" aria-label=\"Read more about Install OpenVPN Server on Ubuntu 26.04, 24.04 &#038; 22.04\">Read more<\/a><\/p>\n","protected":false},"author":10,"featured_media":168622,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[50,75,81],"tags":[282,205,2254,583],"cfg_series":[39802],"class_list":["post-166229","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux-tutorials","category-security","category-ubuntu","tag-linux","tag-security","tag-ubuntu","tag-vpn","cfg_series-ubuntu-2604-security"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166229","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/10"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=166229"}],"version-history":[{"count":1,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166229\/revisions"}],"predecessor-version":[{"id":168623,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166229\/revisions\/168623"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/168622"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=166229"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=166229"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=166229"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=166229"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}