One Public IP Multiple Services: SSLH and HAProxy for SSH VPN and HTTPS in a homelab

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

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.service
DAEMON=/usr/sbin/sslh-select
DAEMON_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-tickets
defaults
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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.

Up ↑