Networking Azure

wstunnel WebSocket Tunnel on Ubuntu 24.04 on Azure User Guide

| Product: wstunnel 10.6.2 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and use of wstunnel on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. wstunnel tunnels arbitrary traffic — TCP, UDP, unix sockets, and dynamic SOCKS5/HTTP proxies — inside a standard WebSocket (or HTTP2) connection. Because that connection looks like ordinary HTTPS web traffic, it passes through firewalls and proxies that would otherwise block the underlying protocol.

The image installs wstunnel 10.6.2 from the pinned upstream release (github.com/erebe/wstunnel, verified by SHA-256), so the exact version is reproducible and there is no third-party package repository on the running VM. wstunnel runs under systemd as the unprivileged wstunnel user. Unattended security upgrades keep the base OS patched.

Secure by design — no default secret. A tunnel that anyone can connect to is an open relay, so this image never ships a fixed or shared secret. On the very first boot of every VM a fresh per-instance tunnel secret (a 48-character token used as the WebSocket HTTP-upgrade path prefix) is generated and stored in /root/wstunnel-credentials.txt (mode 0600, root only). The server rejects the WebSocket upgrade of any client that does not present the correct secret.

Encrypted by default. The server listens on wss:// (TLS) on TCP 8443 using a per-VM self-signed certificate generated on first boot — no shared certificate or private key ships in the image. The secret is read from a root-only EnvironmentFile, so it never appears in the process list or the journal.

What is included:

  • wstunnel 10.6.2 installed as a single static binary at /usr/local/bin/wstunnel, run under systemd as the unprivileged wstunnel user (wstunnel.service)

  • A wss (TLS) WebSocket tunnel server on TCP 8443, supporting forward and reverse tunnels for TCP/UDP and dynamic SOCKS5/HTTP proxies

  • A per-instance tunnel secret generated on first boot, documented in /root/wstunnel-credentials.txt (0600), enforced via --restrict-http-upgrade-path-prefix

  • A per-instance self-signed TLS certificate generated on first boot

  • A hardened systemd unit (NoNewPrivileges, ProtectSystem=strict, empty capability set) that starts only after the per-VM secret and certificate exist

Prerequisites

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

  • Subscription to the wstunnel listing on Azure Marketplace

  • Network Security Group rules allowing TCP 22 (admin) and, for the clients that will use the tunnel, TCP 8443 (the wss server)

  • The wstunnel client binary on the machines that will connect (download the matching release from github.com/erebe/wstunnel/releases)

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). Higher-throughput deployments should use Standard_D2s_v5 or larger.

Step 1: Deploy from the Azure Portal

Search wstunnel in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 8443 from the client networks that will connect to the tunnel, and TCP 22 for administration.

Step 2: Deploy from the Azure CLI

RG="wstunnel-prod"; LOCATION="eastus"; VM_NAME="wstunnel1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/wstunnel-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
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 8443 --priority 1001

Step 3: Verify the tunnel server

SSH into the VM as azureuser and confirm the service is active, running as the unprivileged wstunnel user, and listening on TCP 8443:

sudo systemctl status wstunnel --no-pager
ss -tlnp 2>/dev/null | grep ':8443 '

The wstunnel service active under systemd and running as the unprivileged wstunnel user, wstunnel-cli version 10.6.2, with ss showing the wss TLS WebSocket server listening on TCP port 8443

Step 4: Retrieve the per-VM tunnel secret

The unique tunnel secret for this VM is written to a root-only file on first boot. Read it — the value is what every client must present:

sudo cat /root/wstunnel-credentials.txt

The file records WSTUNNEL_SECRET_PATH (the secret), WSTUNNEL_SERVER_IP (the reachable IP), and WSTUNNEL_SERVER_URL (wss://<ip>:8443). Keep the secret private — anyone who has it can open tunnels through this server.

The security posture: the credentials file is 0600 root root and records a per-VM tunnel secret unique to this VM, the secret is read from a 0640 root wstunnel EnvironmentFile so it never appears on the command line, and a per-VM self-signed TLS certificate and key are present

Step 5: Confirm the secure-by-default authentication

The server tunnels only for clients that present the correct secret. You can prove this on the VM itself — start a short-lived client that forwards a local port through the server to example.com:80 and fetch through it, then repeat with a wrong secret:

timeout 20 bash -c '
wstunnel client --log-lvl WARN \
  -L "tcp://127.0.0.1:2222:example.com:80" \
  --http-upgrade-path-prefix <WSTUNNEL_SECRET_PATH> \
  wss://127.0.0.1:8443 &
CP=$!
for i in $(seq 1 20); do ss -tln | grep -q ":2222 " && break; sleep 0.5; done
curl -s -o /dev/null -w "authenticated tunnel: HTTP %{http_code}\n" -H "Host: example.com" http://127.0.0.1:2222/
kill $CP 2>/dev/null || true
' || true

A request with the correct secret returns HTTP 200 through the tunnel; a client with the wrong secret is rejected by the server during the WebSocket upgrade and no tunnel is established.

The secure-by-default round trip: with the correct per-VM secret a local port is tunnelled through the server to example.com and returns HTTP 200, while a client presenting a wrong secret is rejected at the WebSocket upgrade with no HTTP response

Step 6: Connect from a client machine

Install the matching wstunnel client release on your client machine, then open tunnels against the server. Replace <vm-ip> with your VM's public IP and use the secret from Step 4.

Forward a local TCP port to a remote host through the tunnel (e.g. reach an internal SSH host):

wstunnel client \
  -L 'tcp://127.0.0.1:2222:10.0.0.5:22' \
  --http-upgrade-path-prefix <the-secret-from-step-4> \
  wss://<vm-ip>:8443
# then, in another terminal:  ssh -p 2222 user@127.0.0.1

Start a dynamic SOCKS5 proxy that routes traffic through the tunnel:

wstunnel client \
  -L 'socks5://127.0.0.1:1080' \
  --http-upgrade-path-prefix <the-secret-from-step-4> \
  wss://<vm-ip>:8443
# then point an application at socks5://127.0.0.1:1080

Reverse tunnel — expose a service on your client machine to the server side:

wstunnel client \
  -R 'tcp://8080:127.0.0.1:80' \
  --http-upgrade-path-prefix <the-secret-from-step-4> \
  wss://<vm-ip>:8443

The server uses a self-signed certificate; the wstunnel client accepts it by default (TLS verification is off unless you pass --tls-verify-certificate).

Step 7: Review the secure-by-default service configuration

The systemd unit runs the server as the unprivileged wstunnel user with the secret supplied via an EnvironmentFile, TLS enabled, and a locked-down sandbox:

sudo systemctl cat wstunnel.service | grep -vE '^\s*#|^\s*$'

The wstunnel systemd unit showing the secure defaults: the server runs as the unprivileged wstunnel user with NoNewPrivileges and ProtectSystem strict, TLS certificate and key, the wss listener on 0.0.0.0 port 8443, and a ConditionPathExists gate on the firstboot sentinel so it starts only after the per-VM secret and certificate exist

Restricting the server further

By default the server accepts any authenticated client and forwards to any destination the client requests. To lock it down to specific destinations, edit the unit and add one or more --restrict-to "host:port" flags to the ExecStart line, then reload:

sudo systemctl daemon-reload
sudo systemctl restart wstunnel

For finer-grained rules (per-path matchers, allowed tunnel types) use --restrict-config <yaml>; see the upstream documentation at github.com/erebe/wstunnel.

Managing the service

sudo systemctl status wstunnel      # health
sudo systemctl restart wstunnel     # restart
sudo journalctl -u wstunnel -f      # follow logs

Rotating the tunnel secret

To rotate the secret manually, edit /etc/wstunnel/wstunnel.env, set a new value for WSTUNNEL_RESTRICT_HTTP_UPGRADE_PATH_PREFIX, update /root/wstunnel-credentials.txt for your records, and restart the service. All clients must then use the new secret.

Troubleshooting

  • Clients cannot connect — confirm the Network Security Group allows inbound TCP 8443 from the client network, and that the client uses wss:// (not ws://) and the exact secret from /root/wstunnel-credentials.txt.

  • tls handshake errors on the client — the server certificate is self-signed; either omit --tls-verify-certificate (the default accepts it) or supply your own certificate via --tls-certificate/--tls-private-key on the server.

  • Service inactive on first boot — first boot generates the per-VM secret and certificate before the server starts. Check sudo journalctl -u wstunnel-firstboot and sudo systemctl status wstunnel.

Support

cloudimg images include 24/7 support. Contact support@cloudimg.co.uk for assistance with this image.