Deployment guide

stunnel

Overview

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: it accepts plaintext locally and speaks TLS outwards. This 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.

This SecureAnyCloud image runs stunnel on Ubuntu 24.04 on Azure. It builds stunnel 5.79 from the pinned upstream source (www.stunnel.org/downloads/stunnel-5.79.tar.gz) and verifies the tarball against the upstream published SHA-256 during the build. The exact version is therefore reproducible, and the running VM uses no third-party package repository. stunnel runs under systemd as the unprivileged stunnel user, and unattended security upgrades keep the base OS patched.

No key material ships in the image. A TLS proxy whose private key shipped inside the image would give every customer the same key. On the first boot of every VM, a fresh 256 bit pre-shared key and a fresh TLS certificate and private key are generated. They are unique to that instance and are 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 completes a handshake. The [ssh-tls] tunnel uses stunnel's native pre-shared key authentication. 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 you get:

  • 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 that 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 and documented in /root/stunnel-credentials.txt (0600)
  • A hardened baseline: TLS 1.2 minimum, an 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
  • One short, commented configuration file at /etc/stunnel/stunnel.conf that drives everything

Prerequisites

Before you begin, make sure you have the following:

  • An active Azure subscription, an SSH public key, and a VNet and subnet in the target region
  • A subscription to the stunnel listing on Azure Marketplace
  • Network Security Group rules that allow TCP 22 (administration) 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.

1

Deploy from the Azure Portal

Search Azure Marketplace for stunnel, select the SecureAnyCloud publisher, and select 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.

2

Deploy from the Azure CLI

RG="tls-prod"; LOCATION="eastus"; VM_NAME="tls1" IMAGE_URN="sac1647283583153:stunnel-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:stunnel-ubuntu-24-04:default:latest
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$IMAGE_URN" \
  --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
3

First boot

On first boot, the image generates this VM's pre-shared key and TLS certificate and writes them to /etc/stunnel/psk.txt and /etc/stunnel/certs/stunnel.pem. It 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 lists your pre-shared key identity, the 64 hexadecimal character key, the certificate fingerprint, and the two tunnel endpoints. Keep the file readable by root only. It is the one place the plaintext key exists.

4

Confirm the service is running

stunnel.service runs under systemd as the unprivileged stunnel user. The ss output confirms that 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))

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. SecureAnyCloud does not patch upstream source to correct cosmetic strings.

5

The tunnel requires this VM's pre-shared key

On port 8443, the pre-shared key is checked during the TLS handshake. A client without the key never establishes a session and never reaches the backend.

Read your key from the credentials file, then test three cases. The 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. It shows that the whole path works end to end.

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. To put TLS in front of your own service, copy this block in the configuration file.

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 the file at 0640 root:stunnel, and reload the service.

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 pattern is useful 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.

8

Review the configuration

One short file drives everything. The global block sets the TLS floor, and each [section] is one tunnel. Print the active settings:

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
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, replace <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. To make a tunnel demand the pre-shared key as well as, or instead of, presenting a certificate, add PSKsecrets = /etc/stunnel/psk.txt to its section.

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

10

Confirm no key material ships in the image

No default, shared or built-in key material exists. The credentials file is 0600 root:root. 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
11

Logs and troubleshooting

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

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 that the configuration is well formed and that the backend named in connect is listening. While diagnosing, raise debug = notice to debug = debug for a verbose trace, and then set it back.

12

Security recommendations

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

Keep the pre-shared key requirement on any tunnel exposed beyond your virtual network. It 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 the file at 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 you must update every client 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. Keep connect addresses inside your own network wherever possible.

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

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 SecureAnyCloud image builds the unmodified upstream stunnel 5.79 source. SecureAnyCloud 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.

SecureAnyCloud 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 SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.

Deploy on Azure

Launch stunnel 5.79 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure