Networking Azure

stunnel TLS Proxy on Ubuntu 24.04 on Azure User Guide

| Product: stunnel 5.79 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of stunnel on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. stunnel is a TLS proxy: it wraps an existing plaintext TCP service in encryption without any change to that service. In server mode it accepts TLS connections and forwards the decrypted stream to a local or remote backend. In client mode it does the reverse, accepting plaintext locally and speaking TLS outwards. That makes it the standard way to put modern, hardened TLS in front of a database, a legacy application, an appliance, or anything else that has no encryption of its own.

The image builds stunnel 5.79 from the pinned upstream source (www.stunnel.org/downloads/stunnel-5.79.tar.gz), with the tarball verified against the upstream published SHA-256 during the build, so the exact version is reproducible and there is no third party package repository on the running VM. stunnel runs under systemd as the unprivileged stunnel user. Unattended security upgrades keep the base OS patched.

Secure by design, with nothing baked in. A TLS proxy whose private key shipped inside the image would give every customer the same key, so this image never ships key material. On the very first boot of every VM, a fresh 256 bit pre shared key and a fresh TLS certificate and private key are generated, unique to that instance, and recorded in /root/stunnel-credentials.txt (mode 0600, root only). The daemon is held behind a bootstrap gate until that has happened, so it can never start with missing or shared key material.

The default tunnel demands a credential before it will even complete a handshake. The [ssh-tls] tunnel uses stunnel's native pre shared key authentication, so a client that does not hold this VM's identity and key is rejected during the TLS handshake itself, before any byte reaches the backend. A second tunnel, [ssh-tls-cert], demonstrates classic certificate based TLS termination using the per instance certificate.

What is included:

  • stunnel 5.79 built from the pinned, checksum verified upstream source, run under systemd as the unprivileged stunnel user (stunnel.service)

  • A pre shared key protected TLS tunnel on TCP 8443 and a certificate based TLS termination endpoint on TCP 8444, both wired to this VM's SSH service as a working demonstration backend you repoint at your own service

  • A per instance pre shared key and a per instance TLS certificate and private key generated on first boot, documented in /root/stunnel-credentials.txt (0600)

  • A hardened baseline: TLS 1.2 minimum, unprivileged runtime user, and CAP_NET_BIND_SERVICE granted so you can move a tunnel to a privileged port such as 443 without editing the unit

  • Everything driven from one short, commented configuration file at /etc/stunnel/stunnel.conf

Prerequisites

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

  • Subscription to the stunnel listing on Azure Marketplace

  • Network Security Group rules allowing TCP 22 (admin) and, for the clients that will use the tunnels, TCP 8443 and/or TCP 8444

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). Encryption throughput scales with CPU, so busy tunnels should use Standard_D2s_v5 or larger.

Step 1: Deploy from the Azure Portal

Search stunnel in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 8443 (and TCP 8444 if you will use the certificate endpoint) from your client networks, and TCP 22 for administration.

Step 2: Deploy from the Azure CLI

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

Step 3: First boot

On first boot the image generates this VM's pre shared key and TLS certificate, writes them to /etc/stunnel/psk.txt and /etc/stunnel/certs/stunnel.pem, records both in /root/stunnel-credentials.txt, and only then releases stunnel.service to start. This completes within a few seconds. SSH in as azureuser and read the details:

sudo cat /root/stunnel-credentials.txt

The file names your pre shared key identity, the 64 hexadecimal character key, the certificate fingerprint, and the two tunnel endpoints. Keep it root only; it is the one place the plaintext key exists.

Step 4: Confirm the service is running

stunnel.service runs under systemd as the unprivileged stunnel user. ss confirms both tunnels are listening.

systemctl is-active stunnel.service
ps -o user= -C stunnel | head -1
ss -tlnp | grep -E ':8443 |:8444 '

Expected output:

active
stunnel
LISTEN 0      4096         0.0.0.0:8443      0.0.0.0:*    users:(("stunnel",pid=9769,fd=8))
LISTEN 0      4096         0.0.0.0:8444      0.0.0.0:*    users:(("stunnel",pid=9769,fd=9))

The stunnel service active under systemd and running as the unprivileged stunnel user, with ss showing the pre shared key protected TLS tunnel listening on TCP port 8443 and the certificate based TLS termination endpoint on TCP port 8444

You can also confirm the build, including that pre shared key support is compiled in:

timeout 10 stunnel -version </dev/null 2>&1 | sed -n '1,4p'
Initializing inetd mode configuration
stunnel 5.78 on x86_64-pc-linux-gnu platform
Compiled/running with OpenSSL 3.0.13 30 Jan 2024
Threading:PTHREAD Sockets:POLL,IPv6 TLS:ENGINE,OCSP,PSK,SNI,DTLS

The TLS: line confirms PSK support. The banner reads 5.78 because the upstream 5.79 release ships a stale version constant in src/version.h; the source built here is the 5.79 release, verified by its upstream SHA-256 at build time, and cloudimg does not patch upstream source to correct cosmetic strings.

Step 5: The tunnel requires this VM's pre shared key

This is the security property that matters. On port 8443 the pre shared key is checked during the TLS handshake, so a client without it never establishes a session and never reaches the backend. Read your key from the credentials file, then prove all three cases. These commands run on the VM against loopback; from a remote client, replace 127.0.0.1 with the VM address.

PSK_IDENTITY=$(sudo grep '^PSK_IDENTITY=' /root/stunnel-credentials.txt | cut -d= -f2-)
PSK_KEY=$(sudo grep '^PSK_KEY=' /root/stunnel-credentials.txt | cut -d= -f2-)
echo -n 'no shared key    : '; timeout 8 openssl s_client -connect 127.0.0.1:8443 -quiet -verify_quiet </dev/null 2>/dev/null | head -1 | grep . || echo 'handshake refused'
echo -n 'wrong shared key : '; timeout 8 openssl s_client -connect 127.0.0.1:8443 -quiet -verify_quiet -psk_identity "$PSK_IDENTITY" -psk 00000000000000000000000000000000 </dev/null 2>/dev/null | head -1 | grep . || echo 'handshake refused'
echo -n 'correct key      : '; timeout 8 openssl s_client -connect 127.0.0.1:8443 -quiet -verify_quiet -psk_identity "$PSK_IDENTITY" -psk "$PSK_KEY" </dev/null 2>/dev/null | head -1 | grep . || echo 'handshake refused'

Expected output:

no shared key    : handshake refused
wrong shared key : handshake refused
correct key      : SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.18

The final line is the backend's own greeting arriving through the encrypted tunnel, which is proof the whole path works end to end.

The secure by default pre shared key round trip: a TLS handshake with no shared key and a handshake with the wrong shared key are both refused outright, while the correct per VM key opens the tunnel and the backend service greeting arrives through it

Step 6: Certificate based TLS termination

Port 8444 shows the other pattern, the one most people mean by TLS termination: stunnel presents this VM's certificate, negotiates TLS, and forwards the decrypted stream to the backend. Copy this block in the configuration file to put TLS in front of your own service.

echo -n 'certificate tunnel : '; timeout 8 openssl s_client -connect 127.0.0.1:8444 -quiet -verify_quiet </dev/null 2>/dev/null | head -1 | grep . || echo 'no response'
SERVED_CERT=$(timeout 8 openssl s_client -connect 127.0.0.1:8444 -verify_quiet </dev/null 2>/dev/null | openssl x509 2>/dev/null)
printf '%s\n' "$SERVED_CERT" | openssl x509 -noout -subject -dates -fingerprint -sha256

The certificate is self signed and unique to this VM, so its fingerprint matches the one recorded in /root/stunnel-credentials.txt. For production, replace /etc/stunnel/certs/stunnel.pem with your own certificate and key concatenated into one file, keep it 0640 root:stunnel, and reload the service.

Step 7: SSH through the TLS tunnel from your workstation

Because the demonstration backend is this VM's own SSH service, you can drive the tunnel end to end from a client machine. openssl s_client becomes the SSH transport, so the SSH session travels inside TLS. Replace <vm-ip> with the VM address, and take the identity and key from /root/stunnel-credentials.txt.

ssh -o ProxyCommand="openssl s_client -quiet -connect <vm-ip>:8443 -psk_identity PSK_IDENTITY -psk PSK_KEY" azureuser@<vm-ip>

Substitute PSK_IDENTITY and PSK_KEY with the values from the credentials file. This is a useful pattern in its own right: it presents SSH as an ordinary TLS service, and the pre shared key stops anyone who does not hold it from reaching the SSH port at all.

Step 8: Review the configuration

Everything is driven from one short file. The global block sets the TLS floor, and each [section] is one tunnel.

grep -vE '^\s*;|^\s*$' /etc/stunnel/stunnel.conf

Expected output:

foreground = yes
pid =
output = /var/log/stunnel/stunnel.log
debug = notice
syslog = no
sslVersionMin = TLSv1.2
[ssh-tls]
accept  = 0.0.0.0:8443
connect = 127.0.0.1:22
PSKsecrets = /etc/stunnel/psk.txt
[ssh-tls-cert]
accept  = 0.0.0.0:8444
connect = 127.0.0.1:22
cert    = /etc/stunnel/certs/stunnel.pem

The stunnel configuration file showing the secure defaults: a TLS 1.2 minimum floor, the ssh-tls tunnel requiring the per VM pre shared key file, and the ssh-tls-cert tunnel terminating TLS with the per VM certificate, each mapping an accept port to a backend connect address

Step 9: Put TLS in front of your own service

To wrap one of your own services, change the connect address of a tunnel, or add a new [section]. Anything that speaks plain TCP works, whether it runs on this VM or elsewhere in your virtual network. For example, to add a TLS front door on port 9443 for a PostgreSQL server elsewhere in your virtual network, replacing <private-ip> with that server's address:

printf '\n[postgres-tls]\naccept  = 0.0.0.0:9443\nconnect = <private-ip>:5432\ncert    = /etc/stunnel/certs/stunnel.pem\n' | sudo tee -a /etc/stunnel/stunnel.conf >/dev/null
sudo systemctl reload stunnel

Then open TCP 9443 in the Network Security Group for the clients that need it. Add PSKsecrets = /etc/stunnel/psk.txt to a section if you want that tunnel to demand the pre shared key as well as, or instead of, presenting a certificate.

For the reverse direction, client = yes in a section makes stunnel accept plaintext locally and connect outwards over TLS, which is how you give a legacy application an encrypted path to a remote TLS endpoint.

Step 10: Confirm no key material ships in the image

No default, shared, or baked in key material exists. The credentials file is 0600 root:root, and both the pre shared key file and the certificate bundle are 0640 root:stunnel, readable only by root and the service account.

sudo stat -c 'file: %n  perms: %a  owner: %U:%G' /root/stunnel-credentials.txt /etc/stunnel/psk.txt /etc/stunnel/certs/stunnel.pem
sudo sed -E 's/^(PSK_KEY=.{6}).*/\1********************/' /root/stunnel-credentials.txt | grep -E '^PSK_IDENTITY=|^PSK_KEY='

Expected output:

file: /root/stunnel-credentials.txt  perms: 600  owner: root:root
file: /etc/stunnel/psk.txt  perms: 640  owner: root:stunnel
file: /etc/stunnel/certs/stunnel.pem  perms: 640  owner: root:stunnel

The security proof: the credentials file is 0600 root root while the pre shared key file and the TLS certificate bundle are both 0640 root stunnel, with the per VM key value masked, showing that no shared key material is baked into the image

Step 11: Logs and troubleshooting

stunnel logs to /var/log/stunnel/stunnel.log and to the systemd journal. Connection level detail, including rejected handshakes, appears there.

sudo tail -n 20 /var/log/stunnel/stunnel.log
sudo journalctl -u stunnel.service --no-pager -n 20

If a tunnel does not come up, check the configuration is well formed and that the backend named in connect is actually listening. Raise debug = notice to debug = debug for a verbose trace while diagnosing, then put it back.

Step 12: Security recommendations

  • Restrict the NSG. Allow TCP 8443 and 8444 only from the client networks that need them, and TCP 22 for administration only.

  • Keep the pre shared key requirement on any tunnel exposed beyond your virtual network. It is what stops an unauthenticated peer from reaching the backend at all.

  • Replace the self signed certificate for production. Use a certificate from your own CA or a public CA, concatenate the private key and certificate into /etc/stunnel/certs/stunnel.pem, keep it 0640 root:stunnel, and reload.

  • Rotate the pre shared key by writing a new identity:key line to /etc/stunnel/psk.txt and reloading; the key must be at least 32 hexadecimal characters and every client must be updated at the same time.

  • Keep the TLS floor at 1.2 or raise it. Set sslVersionMin = TLSv1.3 if every client supports it.

  • Do not point a tunnel at a backend you do not control, and keep connect addresses inside your own network wherever possible.

  • Keep the OS patched. Unattended security upgrades remain enabled on the running VM.

Step 13: Support and Licensing

stunnel is developed by Michal Trojnara and distributed under the GNU General Public License version 2 or later, with the copyright holder's explicit exception permitting linking with OpenSSL. This cloudimg image builds the unmodified upstream stunnel 5.79 source. cloudimg provides the packaging, the secure default configuration, the per instance key material automation, security patching, and 24/7 support with a guaranteed 24 hour response SLA.

cloudimg is not affiliated with or endorsed by the stunnel project. stunnel is a mark of its respective owner and is used here only to identify the software.

Deploy on Azure

Find stunnel TLS Proxy on Ubuntu 24.04 LTS on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.