wstunnel WebSocket Tunnel on Ubuntu 24.04 on Azure User Guide
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 unprivilegedwstunneluser (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
wstunnelclient binary on the machines that will connect (download the matching release fromgithub.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 '

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.

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.

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*$'

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://(notws://) and the exact secret from/root/wstunnel-credentials.txt. -
tls handshakeerrors 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-keyon 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-firstbootandsudo systemctl status wstunnel.
Support
cloudimg images include 24/7 support. Contact support@cloudimg.co.uk for assistance with this image.