sslh on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of sslh on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. sslh is an open source SSL/SSH protocol multiplexer. It listens on a single port, reads the first bytes each client sends, works out which protocol is being spoken, and hands the connection to the matching service. The practical result is that SSH and HTTPS share one port.
That matters whenever the network between you and your server only lets 443 out. Hotel, airport, café and captive portal networks, tightly controlled corporate proxies and restrictive egress firewalls routinely allow 443 and block 22. With sslh in front, ssh -p 443 reaches your shell over the one port those networks do allow, while an ordinary browser hitting the same host and the same port still gets your web service. It is equally useful when you simply want one exposed port instead of several on an internet facing host.
The image compiles the latest stable sslh release from the official upstream source tag and bakes it reproducibly (the exact version and its provenance are recorded in /opt/sslh/VERSION). sslh runs under systemd as sslh.service and drops from root to an unprivileged sslh account as soon as it has bound the port.
A working multiplexer the moment it boots. This is not an empty daemon waiting to be configured. The image ships the backends too, so the appliance demonstrates itself standalone with nothing else to install: sslh routes SSH to the VM's own OpenSSH server, HTTPS to a local nginx TLS listener serving a status page, and plain HTTP to a local nginx HTTP listener. You then repoint those routes at your own services.
A per instance TLS certificate, and no key in the image. The HTTPS backend needs a certificate. Rather than baking one in, which would put the same private key on every customer VM, the image ships no certificate and no key at all. Both are generated on your VM at first boot, with Subject Alternative Names covering the VM public IP, hostname and loopback. Both long running services are gated so neither can start before that bootstrap has completed.
No default login of any kind. sslh has no credential to leak: it is a stateless connection router with no account, no password and no token. The only key material the appliance ever holds is the TLS private key described above, and that never ships.
What is included:
-
The latest stable sslh release, compiled from the official upstream source tag and run under systemd as
sslh.service -
A working four way routing table out of the box:
sshto the local OpenSSH server,tlsto the local HTTPS backend,httpto the local HTTP backend, andanyprotas the catch all -
nginx providing both loopback backends, plus an unauthenticated
/healthzload balancer probe and a301redirect to HTTPS on port 80 -
A per instance self signed TLS certificate generated on first boot, with the shipped image containing no certificate and no private key
-
sslh-selftest, a shipped command that opens all three protocols against the multiplexed port and tells you in one line whether each one routed correctly -
The
sslh-selectandsslh-evevent driven backends installed alongside the default, so you can switch without recompiling -
The complete corresponding GPL-2.0 source for the shipped binaries, on the image at
/opt/sslh/src/ -
Ubuntu 24.04 LTS base with latest security patches applied at build time
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the sslh listing on Azure Marketplace
-
The services you eventually want to multiplex, if you are replacing the shipped demonstration backends
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample. sslh is a small C daemon that does very little work per connection, so it is bound by connection rate rather than CPU or memory. For very high connection rates use Standard_D2s_v5 or larger and consider the sslh-ev backend.
Step 1: Deploy from the Azure Portal
Search sslh in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 443 (the multiplexed port carrying SSH, HTTPS and HTTP), TCP 22 (direct admin SSH, which stays available independently of the multiplexer) and TCP 80 (the HTTP to HTTPS redirect and the health probe), each from the networks you connect from.
Step 2: Deploy from the Azure CLI
RG="sslh-prod"; LOCATION="eastus"; VM_NAME="sslh-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/sslh/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name sslh-vnet --address-prefix 10.100.0.0/16 --subnet-name sslh-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name sslh-nsg
az network nsg rule create -g "$RG" --nsg-name sslh-nsg --name allow-mux --priority 100 \
--destination-port-ranges 443 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name sslh-nsg --name allow-ssh --priority 110 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name sslh-nsg --name allow-http --priority 120 \
--destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name sslh-vnet --subnet sslh-subnet --nsg sslh-nsg --public-ip-sku Standard
Step 3: Verify the services and see which process owns which port
Connect over SSH on port 22, then confirm the three services are active and look at the listener table. This is the clearest picture of how the appliance is put together: sslh alone holds the public port 443, while the backends it routes to are bound to the loopback interface where nothing outside the VM can reach them directly.
sudo systemctl is-active sslh nginx ssh
sudo ss -tlnp | grep -E ':443|:8443|:8080'
cat /opt/sslh/VERSION
systemctl is-active prints active three times. In the listener table you will see sslh on 0.0.0.0:443 and nginx on 127.0.0.1:8443 and 127.0.0.1:8080. /opt/sslh/VERSION records the exact upstream tag the binary was compiled from.

Step 4: Prove that one port is carrying three protocols
This is the product. Open three connections to the same port 443 and watch each one land on a different service. The TLS connection reaches the HTTPS backend, the SSH connection gets a real OpenSSH protocol banner, and the plain HTTP connection reaches the HTTP backend:
curl -sk -o /dev/null -w 'TLS on 443 -> HTTP %{http_code}\n' https://127.0.0.1:443/
curl -sk https://127.0.0.1:443/ | grep -q 'TLS backend' && echo "TLS on 443 -> routed to the HTTPS backend"
timeout 10 bash -c 'exec 3<>/dev/tcp/127.0.0.1/443; printf "SSH-2.0-cloudimg_demo\r\n" >&3; IFS= read -t 8 -r l <&3; printf "%s\n" "$l"' | tr -d '\r'
curl -s http://127.0.0.1:443/ | grep -q 'HTTP backend' && echo "HTTP on 443 -> routed to the HTTP backend"
The third command is worth understanding. sslh identifies SSH from the client's own identification string, so the probe writes SSH-2.0-... first and then reads. What comes back is the banner of the VM's OpenSSH server, for example SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.18, which is proof that port 443 handed that connection to sshd.
The image also ships a single command that runs all three checks and fails loudly if any route is wrong:
sudo /usr/local/sbin/sslh-selftest
It prints one sslh-selftest OK: line naming each backend it reached, and exits non zero with a FAIL reason if a route is broken. Use it after any configuration change.

Step 5: Connect over SSH on port 443 from your own machine
Everything above ran on the VM. The point of sslh is what it lets you do from outside, over a network that only permits 443. From your workstation:
ssh -p 443 azureuser@<public-ip>
curl -k https://<public-ip>/
Both use port 443 and both work. To make ssh -p 443 the default for this host, add it to your ~/.ssh/config:
Host my-sslh-host
HostName <public-ip>
User azureuser
Port 443
Port 22 also remains open and working, so you never lose direct administrative access if you change the multiplexer configuration.
Step 6: Understand the routing configuration
All of the behaviour above comes from one file, /etc/sslh/sslh.cfg:
sudo cat /etc/sslh/sslh.cfg
The listen block is the single public port. The protocols block is the routing table, and order matters: sslh tries each probe in turn and uses the first that matches, so the anyprot catch all must stay last. on-timeout: "ssh" covers protocols where the server speaks first and the client sends nothing, and user: "sslh" is the unprivileged account sslh permanently drops to once it has bound port 443.

Step 7: Point sslh at your own services
The shipped backends exist so the appliance works standalone. To multiplex your own services, change the host and port of the tls and http entries to wherever your services actually listen. They can be anywhere sslh can reach, on the VM or on another host. Find the lines to edit:
sudo grep -nE 'name: "(tls|http)"' /etc/sslh/sslh.cfg
Edit those lines in /etc/sslh/sslh.cfg (for example point tls at 127.0.0.1 port 8443 on your own web server, or at another host entirely), then apply and re-prove:
sudo systemctl restart sslh
Run sudo /usr/local/sbin/sslh-selftest again afterwards. Note that its TLS and HTTP checks look for the shipped demonstration pages, so once you have repointed those routes at your own services the selftest will correctly report a mismatch: at that point verify with curl against your own service instead.
Adding more protocols. sslh can also probe OpenVPN, XMPP, TINC, SOCKS and others, and can match on TLS ALPN or SNI so several TLS services share the port. Add further entries to the protocols block in the same shape, keeping anyprot last. Run man sslh on the VM for the full list of probes and options.
Step 8: Use your own domain and a trusted certificate
The per instance self signed certificate is a convenience for getting started, and a browser will warn about it. For production, point a DNS name you control at the VM public IP and install a certificate for that name:
sudo cp /path/to/fullchain-<your-domain>.pem /etc/nginx/tls/cert.pem
sudo cp /path/to/privkey-<your-domain>.pem /etc/nginx/tls/key.pem
sudo systemctl reload nginx
If you have repointed the tls route at your own service rather than the shipped nginx backend, install the certificate on that service instead. Either way sslh is unaffected: it never terminates TLS, it only recognises TLS and forwards the connection untouched, so your certificate and private key are only ever seen by the service that terminates them.
Step 9: Server components
| Component | Version / Detail |
|---|---|
| Multiplexer | sslh (latest stable upstream tag, compiled reproducibly, see /opt/sslh/VERSION) |
| Binaries | /usr/local/sbin/sslh (sslh-fork), plus sslh-select and sslh-ev |
| Public port | 0.0.0.0:443 held by sslh, routing ssh, tls, http, anyprot |
| SSH backend | the VM's own OpenSSH server on 127.0.0.1:22 (also reachable directly on port 22) |
| TLS backend | nginx on 127.0.0.1:8443, per instance certificate in /etc/nginx/tls/ |
| HTTP backend | nginx on 127.0.0.1:8080 |
| Front helper | nginx on :80 (unauthenticated /healthz, 301 to HTTPS) |
| Configuration | /etc/sslh/sslh.cfg |
| Self test | /usr/local/sbin/sslh-selftest |
| Privileges | binds 443 as root, drops permanently to the sslh account |
| Operating system | Ubuntu 24.04 LTS (patched at build) |
| License | GPL-2.0 (sslh), source at /opt/sslh/src/ |

Step 10: Managing the sslh service
sudo systemctl is-active sslh nginx
sudo journalctl -u sslh --no-pager | tail -5
Apply a configuration change with sudo systemctl restart sslh and follow the logs live with sudo journalctl -u sslh -f. A summary of this VM's endpoints is written to /root/sslh-credentials.txt on first boot; read it with sudo cat /root/sslh-credentials.txt.
To raise log detail, add verbose-connections: 1; to /etc/sslh/sslh.cfg and restart, which logs the protocol decision for each connection. This is the first place to look if a client is being routed to the wrong backend.
Switching backend. sslh-fork forks a process per connection and is the default. For very high connection rates, point the ExecStart line in /etc/systemd/system/sslh.service at /usr/local/sbin/sslh-ev or /usr/local/sbin/sslh-select, which handle all connections in a single process, then sudo systemctl daemon-reload and restart.
Step 11: Security recommendations
-
sslh does not add authentication and does not terminate TLS. It routes connections. Every backend keeps its own security exactly as it had it, so keep SSH key only authentication and keep your TLS service's certificate configuration correct.
-
Restrict the NSG. Allow 443 from the networks your users connect from. Keep 22 restricted to your management range so you always have an administrative route that does not depend on the multiplexer.
-
Backends stay on the loopback. The shipped backends are bound to
127.0.0.1so they cannot be reached except through sslh. If you repoint a route at another host, restrict that host's firewall to the sslh VM. -
Client IP addresses. Backends see connections coming from the sslh VM rather than the original client, because transparent mode is not enabled by default (it needs additional routing rules). If a backend needs the true client address, enable sslh's transparent proxy mode as described in
man sslh, or use the PROXY protocol where the backend supports it. -
Use a trusted certificate and your own domain (Step 8) so clients validate the service without a manual trust step.
-
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Step 12: Support and Licensing
sslh is distributed under the GNU General Public License version 2. Because the GPL is a copyleft licence, this image carries both of the things that licence requires: the verbatim licence text at /opt/sslh/LICENSE, and the complete corresponding source for the binaries it runs at /opt/sslh/src/, with rebuild instructions in /opt/sslh/README.cloudimg. sslh is redistributed unmodified.
cloudimg provides the packaging, the systemd integration, the working demonstration backends, the per instance TLS bootstrap, the self test tooling, and 24/7 support with a guaranteed 24 hour response SLA. sslh is an independent open source project by Yves Rutschle and this image is not affiliated with or endorsed by the sslh project or its authors.
Deploy on Azure
Find sslh on Ubuntu 24.04 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.