Problem to solve
I have many webservices and vpn and other service that I want to be able to access from Internet in my homelab. But the issue here is that I only have one external ip. And I want to with opensource applications. The only thing that is not Opensource is external firewall.
I want to be able to acess below whit only 443 and 53 from internet
- SSH to server
- SSH to 443 to server
- SSH to server with real TLS (bypass FW) How to set that up https://blog.invid.eu/2026/02/04/when-the-firewall-says-no-but-port-443-says-yes-how-to-tunnel-ssh-and-vpn-access-through-a-firewall-using-tls/
- Access webbservices1
- Access webbservices2
- Access webbservices3
- Access *.domain2.se
- Access *.domain.se
- VPN Wireguard (OpenSense)
- VPN openVPN (OpenSense)
Picture says more than 1000 words so lets look at the solution.
The picture can be downloaded from: https://github.com/RogerBergling/sslh-haproxy/tree/main

Steps that I took. If you need help just send me an email and I will try to help if you get stuck.
- Configure External FW
- Setup SSLH
- Setup haproxy
- Setup internal FW
Architecture overview before the configs
Incoming traffic first reaches the external firewall. The firewall forwards TCP/443 to SSLH which identifies whether the connection is SSH, OpenVPN or TLS. TLS traffic is then sent to HAProxy which performs SNI-based routing. Depending on the requested hostname HAProxy either passes TLS through unchanged or terminates TLS and routes the request to an internal web service.
Why SSLH?
HAProxy can inspect and route TLS connections, but it cannot distinguish between SSH and TLS on the same port. SSLH solves this by identifying protocols based on their initial packets and forwarding each connection to the correct service.This allows SSH, OpenVPN and HTTPS to share port 443.
SSLH config
# Default options for sslh initscript# sourced by /etc/init.d/sslh# binary to use: forked (sslh) or single-thread (sslh-select) version# systemd users: don't forget to modify /lib/systemd/system/sslh.serviceDAEMON=/usr/sbin/sslh-selectDAEMON_OPTS="--user sslh --listen 0.0.0.0:443 --ssh 172.1.1.11:22 --ssl 172.1.1.10:443 --proxy-protocol --openvpn 172.1.1.253:1194 --timeout 5"
Haproxy config
global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin stats timeout 30s user haproxy group haproxy daemon # Default SSL material locations ca-base /etc/ssl/certs crt-base /etc/ssl/private # SSL configuration ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-ticketsdefaults log global mode http option dontlognull timeout connect 5000 timeout client 50000 timeout server 50000 errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http errorfile 500 /etc/haproxy/errors/500.http errorfile 502 /etc/haproxy/errors/502.http errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http # ============================================================ # INCOMING HTTPS FROM SSLH # ============================================================ frontend ft_https bind *:443 mode tcp tcp-request inspect-delay 1s tcp-request content accept if { req_ssl_hello_type 1 } tcp-request content capture req.ssl_sni len 100 log-format "src=%ci dst=%fi:%fp sni=%[capture.req.hdr(0)]" # ---------------------------------------------------- # REMOTE - TLS PASSTHRU # ---------------------------------------------------- use_backend bk_remote if { req_ssl_sni -i remote.domain.se } # ---------------------------------------------------- # WEBSERVICES - TLS PASSTHROUGH # ---------------------------------------------------- use_backend bkwebservices if { req_ssl_sni -i webservices.domain.se } use_backend bkwebservices if { req_ssl_sni -m end .webservices.domain.se } # ---------------------------------------------------- # SECOND DOMAIN - TLS PASSTHROUGH # ---------------------------------------------------- use_backend bk_domain2 if { req_ssl_sni -m end .domain2.se } # ---------------------------------------------------- # WEBSERVICES SERVICES # ---------------------------------------------------- use_backend bk_services_tls if { req_ssl_sni -i webservices1.domain.se } use_backend bk_services_tls if { req_ssl_sni -i webservices2.domain.se } use_backend bk_services_tls if { req_ssl_sni -i webservices1.domain.se } # ---------------------------------------------------- # DEFAULT # ---------------------------------------------------- default_backend bk_default # ============================================================ # WEBSEVICES.NET - COMMON TLS TERMINATION # ============================================================ frontend ft_services_tls # Force HTTP/1.1. # Prevents HTTP/2 connection coalescing between # the webservices services and other hosts. bind 127.0.0.1:8443 ssl crt /etc/haproxy/certs/cert.pem alpn http/1.1 mode http option httplog # ---------------------------------------------------- # ORIGINAL CONNECTION WAS HTTPS # ---------------------------------------------------- http-request set-header X-Forwarded-Proto https http-request set-header X-Forwarded-Port 443 # ---------------------------------------------------- # WEBSERVICES SHOULD NEVER BE HERE # ---------------------------------------------------- # # If an WEBSERVICES host somehow arrives on this TLS # termination frontend, return 421 instead of # "Unknown domain.se service". # http-request return status 421 content-type "text/plain" string "Misdirected request" if { hdr(host) -m end .webservices.domain.se } # ---------------------------------------------------- # WEBSERVICES1 # ---------------------------------------------------- use_backend bk_webservice1 if { hdr(host) -i webservices1.domain.se } # ---------------------------------------------------- # # WEBSERVICES2 # ---------------------------------------------------- use_backend bk_webservices2 if { hdr(host) -i webservices2.domain.se } # ---------------------------------------------------- # # WEBSERVICES3 # ---------------------------------------------------- use_backend bk_webservices3 if { hdr(host) -i webservices3.domain.se } # ---------------------------------------------------- # UNKNOWN HOST # ---------------------------------------------------- default_backend bk_webservices_default # ---------------------------------------------------- # WEBSERVICES SHOULD NEVER BE HERE # ---------------------------------------------------- backend bk_remote mode tcp option tcp-check server remote 192.16.4.50:443 # ============================================================ # WEBSERVICES - TLS PASSTHROUGH # ============================================================ backend bkwebservices mode tcp option tcp-check server server4 192.16.4.10:443 # ============================================================ # SECOND DOMAIN - TLS PASSTHROUGH # ============================================================ backend bk_domain2 mode tcp option tcp-check server domain2 192.16.4.30:443 # ============================================================ # DEFAULT TCP BACKEND # ============================================================ backend bk_default mode tcp tcp-request content reject # ============================================================ # TCP BRIDGE TO TLS TERMINATION # ============================================================ backend bk_services_tls mode tcp server tls 127.0.0.1:8443 # ============================================================ # WEBBSERVICES1 # ============================================================ backend bk_webservice1 mode http option httpchk server server 192.16.4.40:8000 # ============================================================ # WEBBSERVICES2 # ============================================================ backend bk_webservices2 mode http option httpchk server server2 192.16.4.40:8001 # ============================================================ # WEBBSERVICES3 # ============================================================ backend bk_webservices3 mode http option httpchk server server3 192.16.4.40:8002 # ============================================================ # DEFAULT WEBSERVICES BACKEND # ============================================================ backend bk_webservices_default mode http http-request return status 404 content-type "text/plain" string "Unknown domain.se service"
Conclusion
Using SSLH and HAProxy I was able to expose SSH, OpenVPN, WireGuard and multiple HTTPS services from a homelab while using a single public IPv4 address. The design requires only ports 443 and 53 to be reachable from the Internet and relies entirely on open-source software except for the external firewall.
One picture was AI generated, you can guess wish one. ๐
That was all for today!
Keep hacking
//Roger
Leave a Reply