Oc
Applications Azure

OpenVPN Community on Ubuntu 24.04 on Azure User Guide

| Product: OpenVPN Community 2.6 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of OpenVPN Community on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. OpenVPN Community Edition is the classic open source SSL/TLS VPN daemon: a routed, certificate authenticated VPN server that gives remote clients a secure encrypted tunnel into your Azure network and out to the internet.

This image ships only the GPL 2.0 Community Edition (never the proprietary Access Server). It installs the current stable OpenVPN 2.6 line from the Ubuntu 24.04 archive, which is actively security patched through the Ubuntu update stream, so unattended security upgrades keep the daemon patched on your running VM.

Security by design — no shipped keys, a per instance PKI. A VPN image that ships baked in private keys would let anyone with the image impersonate the server or decrypt traffic, so this image never ships any key material. On the very first boot of every VM a fresh per instance easy-rsa PKI is generated (an elliptic curve CA, a server certificate and key, a tls-crypt control channel key, and an initial client certificate). No two VMs ever share key material, and nothing private exists in the captured image. The certificate authority, server and client certificates, and the tls-crypt key live on a dedicated data disk at /var/lib/openvpn-pki.

Ready to connect out of the box. First boot also assembles a complete, inline client configuration at /root/client1.ovpn (mode 0600, root only) whose remote line already points at this VM public IP. Copy that file to a client machine and connect immediately, then add your own per client certificates for real users.

What is included:

  • OpenVPN 2.6 (current stable line) from the Ubuntu 24.04 archive, run under systemd as the packaged template unit openvpn-server@server, dropping to the unprivileged nobody user after reading its keys

  • A routed UDP VPN on port 1194 with modern crypto: data-ciphers AES-256-GCM, a tls-crypt protected control channel, TLS 1.2 minimum, and an elliptic curve (secp384r1) PKI

  • A per instance PKI generated on first boot (elliptic curve CA, server and client certificates, tls-crypt key) — no shared or shipped keys — stored on a dedicated 20 GiB Azure data disk at /var/lib/openvpn-pki

  • A ready to use inline client config at /root/client1.ovpn whose remote already points at your VM public IP

  • Internet egress for VPN clients baked in as static configuration: IP forwarding plus an nftables masquerade rule, both persisted across reboot

  • A tiny nginx on port :80 serving only an unauthenticated /healthz endpoint (HTTP 200) for Azure Load Balancer health probes — this is not a VPN management UI

  • Ubuntu 24.04 LTS base with latest security patches applied at build time, and unattended security upgrades kept enabled

  • 24/7 cloudimg support with a guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region

  • Subscription to the OpenVPN Community listing on Azure Marketplace

  • Network Security Group rules allowing TCP 22 (admin) and UDP 1194 (the VPN) from the clients that will connect

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for a small remote access VPN. Higher throughput or many concurrent clients should use Standard_D2s_v5 or larger.

Step 1: Deploy from the Azure Portal

Search OpenVPN Community in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow UDP 1194 (the VPN) from your client networks and TCP 22 for administration.

Step 2: Deploy from the Azure CLI

RG="vpn-prod"; LOCATION="eastus"; VM_NAME="vpn1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/openvpn-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
# Open the VPN and admin ports on the VM NSG (OpenVPN uses UDP 1194 by default):
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 1194 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22   --priority 1002

OpenVPN uses UDP 1194 by default. Make sure the NSG rule uses the UDP protocol, not TCP.

Step 3: First boot

On first boot the image resolves the VM public IP, generates a fresh per instance easy-rsa PKI (CA, server and client certificates, and the tls-crypt key), brings up the openvpn-server@server service, proves the tunnel works end to end, assembles the ready to use /root/client1.ovpn, and writes /root/openvpn-credentials.txt. This completes within a minute. SSH in as azureuser and read the summary:

sudo cat /root/openvpn-credentials.txt

The summary records this VM public IP, the VPN subnet (10.8.0.0/24), the CA fingerprint, and the location of the client config and PKI.

Step 4: Confirm the VPN server is running

The openvpn-server@server service and the nginx health endpoint are both active, the server holds the VPN endpoint address 10.8.0.1 on tun0, and the daemon is listening on UDP 1194:

systemctl is-active openvpn-server@server nginx.service
ip -4 addr show tun0
ss -lunp | grep ':1194 '

The openvpn-server@server and nginx services both active under systemd, the systemctl status showing the service loaded with the cloudimg drop in and running, the tun0 interface holding the VPN server endpoint 10.8.0.1/24, and the listener table showing the VPN on UDP port 1194 and the nginx health endpoint on TCP port 80

The unauthenticated health endpoint used by Azure Load Balancer probes returns HTTP 200:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz

Step 5: Inspect the per instance PKI

The certificate authority, server and client certificates, and the tls-crypt key were generated on this VM first boot and live on the dedicated data disk at /var/lib/openvpn-pki. Private keys are root only (mode 0600). Nothing here was shipped in the image:

sudo findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/openvpn-pki
sudo openssl x509 -in /var/lib/openvpn-pki/pki/ca.crt -noout -subject -dates -fingerprint -sha256
sudo stat -c '%n %a %U:%G' /var/lib/openvpn-pki/pki/private/server.key /var/lib/openvpn-pki/tls-crypt.key

The per instance easy-rsa PKI on the dedicated data disk mounted at /var/lib/openvpn-pki, the certificate authority listing with the ca.crt, issued and private directories, the CA certificate subject issuer validity dates and SHA256 fingerprint showing it was generated for this VM on first boot, and the key material file modes showing the server key and tls-crypt key are 0600 root only and nothing was shipped in the image

Step 6: The ready to use client config

A complete inline client configuration for this VM is waiting at /root/client1.ovpn. Its remote line already points at this VM public IP, and the CA, client certificate, client key, and tls-crypt key are all embedded inline. Copy it to a client machine and connect:

sudo cat /root/client1.ovpn

The generated client1.ovpn inline client configuration with the client directives, the UDP remote pointing at the VM public IP on port 1194, remote-cert-tls server, data-ciphers AES-256-GCM, and the inline certificate blocks, with the private key block and the tls-crypt block redacted for display because they are unique per VM material that is never shown and never shipped

The <key> and <tls-crypt> blocks in the screenshot above are redacted for display only. The real file contains the live per instance private material and is why it is stored root only.

Step 7: Prove the tunnel works

From any client that has client1.ovpn, connect with sudo openvpn --config client1.ovpn and then, in a second terminal, ping 10.8.0.1 to reach the server across the tunnel. The client completes the TLS handshake, receives a 10.8.0.x address on its tun interface, and reaches the server VPN address 10.8.0.1 inside the encrypted tunnel.

The block below runs that whole exchange as a self contained quick self test: it connects a client to the server over loopback on a separate tun9 interface, confirms the client received a VPN address, pings the server across the tunnel, then disconnects:

sudo bash -c '
CONF=$(mktemp /tmp/selftest.XXXX.ovpn)
sed -e "s/^remote .*/remote 127.0.0.1 1194/" /root/client1.ovpn > "$CONF"; echo route-nopull >> "$CONF"
LOG=$(mktemp /tmp/selftest.XXXX.log)
openvpn --config "$CONF" --dev tun9 --daemon --log "$LOG" --writepid /tmp/selftest.pid
for i in $(seq 1 30); do ip -4 addr show tun9 2>/dev/null | grep -q 10.8.0. && break; sleep 1; done
CIP=$(ip -4 addr show tun9 2>/dev/null | grep -oE "10\.8\.0\.[0-9]+" | head -1)
echo "client connected: tun9 = $CIP"
ping -c 3 10.8.0.1
kill "$(cat /tmp/selftest.pid 2>/dev/null)" 2>/dev/null; rm -f "$CONF" "$LOG" /tmp/selftest.pid
'

A real OpenVPN client connection log showing OpenVPN 2.6.19, the TLS 1.3 control channel negotiated with an elliptic curve secp384r1 peer certificate, VERIFY OK for the server certificate, the pushed configuration including redirect-gateway and DNS, the client receiving the address 10.8.0.2 on tun9, Initialization Sequence Completed, the AES-256-GCM data channel, and three successful pings to the server 10.8.0.1 across the tunnel with zero packet loss

Because the server pushes redirect-gateway, a normally connected client also sends its internet traffic through the VPN, egressing via this VM (IP forwarding and an nftables masquerade are configured in the image).

Step 8: Add and revoke clients

Sharing client1 is fine for a quick test, but give each real user their own certificate. Build a new client certificate with easy-rsa against the per instance PKI, then assemble an inline .ovpn for it:

export EASYRSA_PKI=/var/lib/openvpn-pki/pki
sudo -E /usr/share/easy-rsa/easyrsa build-client-full alice nopass

To revoke a client and stop it connecting, revoke the certificate, regenerate the CRL, enable crl-verify in the server config, and restart the service:

export EASYRSA_PKI=/var/lib/openvpn-pki/pki
sudo -E /usr/share/easy-rsa/easyrsa revoke alice
sudo -E /usr/share/easy-rsa/easyrsa gen-crl
echo "crl-verify /var/lib/openvpn-pki/pki/crl.pem" | sudo tee -a /etc/openvpn/server/server.conf
sudo systemctl restart openvpn-server@server

Step 9: Harden for production

  • Give every user their own certificate (Step 8) and never share client1 beyond an initial test.

  • Scope what is routed. The default pushes redirect-gateway (full tunnel). For split tunnel, remove that push and push only the routes your clients need in /etc/openvpn/server/server.conf.

  • Set your own DNS. The image pushes public resolvers; push your internal DNS with push "dhcp-option DNS <ip>" for split horizon name resolution.

  • Restrict the NSG. Allow UDP 1194 only from the client networks that need it, and keep TCP 22 limited to your admin range.

  • Keep the PKI safe. It lives on the data disk at /var/lib/openvpn-pki; back up the CA and snapshot the disk as part of your DR plan.

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. Contact support@cloudimg.co.uk.

OpenVPN is a registered trademark of OpenVPN Inc. cloudimg is not affiliated with or endorsed by OpenVPN Inc. This image ships the open source OpenVPN Community Edition (GPL 2.0), not the proprietary Access Server. All product and company names are trademarks or registered trademarks of their respective holders and are used only to identify the software.