adllm Insights logo adllm Insights logo

Configuring HAProxy for SSL/TLS Passthrough with SNI-based Backend Selection for Non-HTTP Protocols

Published on by The adllm Team. Last modified: . Tags: HAProxy SSL TLS SNI Load Balancing TCP Networking Non-HTTP Security

Modern network architectures often require sophisticated routing of encrypted traffic. While HAProxy is renowned for HTTP/HTTPS load balancing, its capabilities extend significantly into Layer 4 TCP proxying, including SSL/TLS passthrough for non-HTTP protocols. This mode allows backend servers to handle SSL/TLS termination directly, which is crucial for end-to-end encryption integrity or when backend services manage their own certificates for protocols like MQTT, XMPP, secure email (IMAPS, SMTPS), or other custom TCP services.

A key challenge in such scenarios is directing incoming TLS connections to the correct backend service pool when multiple services share a single public IP address and port (e.g., port 443). Server Name Indication (SNI), an extension to the TLS protocol defined in RFC 6066, provides the solution by allowing the client to specify the hostname it’s trying to reach during the TLS handshake. HAProxy can inspect this SNI value without decrypting the traffic and make intelligent routing decisions.

This article provides a comprehensive guide for configuring HAProxy to perform SSL/TLS passthrough with SNI-based backend selection, focusing specifically on non-HTTP protocols. We’ll cover the core concepts, detailed configuration steps, practical examples, testing methods, and common pitfalls.

Core Concepts Explained

Before diving into the configuration, let’s clarify the key technologies:

  • HAProxy: A high-performance, open-source TCP/HTTP load balancer and proxy server.
  • SSL/TLS Passthrough: HAProxy forwards encrypted traffic directly to backend servers without decrypting it. The TLS handshake occurs between the client and the backend server.
  • SNI (Server Name Indication): A TLS extension where the client indicates the hostname it wishes to connect to. This information is in the ClientHello message, which is unencrypted at the start of the TLS handshake.
  • Non-HTTP Protocols: Any TCP-based application protocol other than HTTP/S, such as MQTT, XMPP, IMAPS, SMTPS, FTPS, or custom binary protocols that use TLS for security.
  • mode tcp: HAProxy’s operational mode for Layer 4 load balancing, essential for SSL/TLS passthrough. See HAProxy Configuration Manual - mode.
  • req_ssl_sni: An HAProxy fetch method to extract the SNI hostname from the client’s TLS handshake. See HAProxy Configuration Manual - req_ssl_sni.

Why Use SSL/TLS Passthrough with SNI?

This configuration offers several advantages:

  • End-to-End Encryption: The proxy does not decrypt the traffic, preserving a true end-to-end encrypted channel between the client and the backend server.
  • Simplified Certificate Management on Proxy: SSL/TLS certificates and private keys are managed solely on the backend servers. HAProxy doesn’t need access to them for passthrough.
  • Support for Non-HTTP Protocols: Effectively routes various TLS-secured services that are not HTTP-based.
  • Resource Efficiency on Proxy: SSL/TLS passthrough consumes fewer CPU resources on HAProxy compared to SSL/TLS termination, as no cryptographic operations are performed by the proxy.
  • Centralized Entry Point: Multiple distinct services, each with its own TLS certificate and hostname, can share a single public IP address and port.

Prerequisites

  • HAProxy Version: HAProxy 1.5 or later is required for SNI support in mode tcp. Using a recent stable version (e.g., 2.4, 2.6, 2.8 or newer, available from the HAProxy Download Page) is highly recommended for the latest features and security fixes.
  • Backend Server Configuration: Backend servers must be configured to handle their respective non-HTTP protocols and perform SSL/TLS termination using appropriate certificates for the hostnames they serve.
  • Understanding of TCP and TLS: Familiarity with TCP/IP networking and the TLS handshake process is beneficial.

HAProxy Configuration Steps

Configuring HAProxy for SNI-based SSL/TLS passthrough involves several key directives primarily within a frontend section operating in mode tcp.

1. Set mode tcp

The frontend handling the incoming TLS connections must operate in mode tcp.

1
2
3
4
frontend ft_secure_passthrough
    bind *:443
    mode tcp
    option tcplog # Useful for detailed TCP connection logging

This configuration makes HAProxy listen on port 443 for any TCP traffic.

2. Inspect ClientHello for SNI

To reliably capture the SNI, HAProxy needs to wait for the client to send its ClientHello message. This is achieved using tcp-request inspect-delay and tcp-request content accept.

1
2
3
4
    # Wait up to 5 seconds for the ClientHello message
    tcp-request inspect-delay 5s
    # Only proceed if the incoming data is a TLS ClientHello (type 1)
    tcp-request content accept if { req_ssl_hello_type 1 }

Without inspect-delay, HAProxy might try to read the SNI before the client has sent enough data. req_ssl_hello_type 1 ensures HAProxy processes data only when it identifies a ClientHello message. The condition { req_ssl_hello_type 1 } checks if the SSL record type is ClientHello.

3. Define ACLs Based on SNI

Access Control Lists (ACLs) are used to match the SNI value extracted by req_ssl_sni. See HAProxy Configuration Manual - acl.

1
2
3
4
5
6
    # ACL for an MQTT service
    acl mqtt_service req_ssl_sni -i mqtt.example.com
    # ACL for an XMPP service (matches specific hostname)
    acl xmpp_service req_ssl_sni -i xmpp.chat.org
    # ACL for any service under a subdomain using suffix match
    acl internal_services req_ssl_sni -m end .internal.example.net

The -i flag makes the hostname match case-insensitive. The -m end flag performs a suffix match. Other match types like -m beg (prefix) or -m sub (substring) are also available.

4. Route Traffic Using use_backend

Based on the ACLs, use_backend directives route the connection to the appropriate backend server pool. The order matters; the first matching use_backend rule is applied.

1
2
3
    use_backend bk_mqtt if mqtt_service
    use_backend bk_xmpp if xmpp_service
    use_backend bk_internal_generic if internal_services

5. Specify a default_backend

A default_backend handles connections that do not match any SNI ACLs or for clients that do not send SNI.

1
    default_backend bk_default_passthrough

This backend could route to a default service, an error page, or simply drop the connection depending on policy.

6. Configure Backend Server Pools

Each backend pool defines the servers for a specific service. They must also operate in mode tcp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
backend bk_mqtt
    mode tcp
    balance roundrobin
    # Backend servers handle their own SSL/TLS for mqtt.example.com
    server mqtt_srv1 10.0.1.10:8883 check
    server mqtt_srv2 10.0.1.11:8883 check

backend bk_xmpp
    mode tcp
    balance roundrobin
    # Backend servers handle their own SSL/TLS for xmpp.chat.org
    server xmpp_srv1 10.0.2.10:5223 check

backend bk_internal_generic
    mode tcp
    balance roundrobin
    # Backend servers for *.internal.example.net
    server internal_srvA 10.0.3.10:9000 check
    server internal_srvB 10.0.3.11:9000 check

backend bk_default_passthrough
    mode tcp
    # A default destination or an error service
    server default_srv 10.0.9.1:443 check

Backend servers are responsible for their specific non-HTTP protocol and SSL/TLS termination on their respective ports (e.g., 8883 for secure MQTT, 5223 for secure XMPP client-to-server). The check option enables basic TCP health checks.

Complete HAProxy Configuration Example

Here’s a consolidated example for haproxy.cfg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    # Path to admin socket may vary by distribution
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

defaults
    log     global
    mode    http # Default mode, overridden by frontend/backend
    option  httplog
    option  dontlognull
    timeout connect 5000
    timeout client  50000
    timeout server  50000
    # Paths to error files may vary by distribution
    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

frontend ft_secure_passthrough
    bind *:443
    mode tcp
    option tcplog # Enable TCP logging for this frontend

    # Inspect SNI from ClientHello
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    # ACLs for SNI-based routing
    acl mqtt_traffic req_ssl_sni -i mqtt.example.com
    acl xmpp_traffic req_ssl_sni -i xmpp.chat.org
    acl mail_traffic req_ssl_sni -i imap.example.org

    # Backend selection based on SNI ACLs
    # Order is important; first match wins.
    use_backend bk_mqtt_passthrough if mqtt_traffic
    use_backend bk_xmpp_passthrough if xmpp_traffic
    use_backend bk_mail_passthrough if mail_traffic

    # Default backend if no SNI matches or SNI is not sent
    default_backend bk_default_reject

backend bk_mqtt_passthrough
    mode tcp
    balance roundrobin
    # Ensure MQTT servers on 10.0.1.x are listening on port 8883
    # and configured for TLS using certificates for mqtt.example.com
    server mqtt_server1 10.0.1.10:8883 check
    server mqtt_server2 10.0.1.11:8883 check

backend bk_xmpp_passthrough
    mode tcp
    balance roundrobin
    # XMPP server on 10.0.2.10 listens on 5223 (secure C2S)
    # with TLS certs for xmpp.chat.org
    server xmpp_server1 10.0.2.10:5223 check

backend bk_mail_passthrough
    mode tcp
    balance roundrobin
    # Secure IMAP server on 10.0.3.10 listens on 993
    # with TLS certs for imap.example.org
    server imap_server1 10.0.3.10:993 check

backend bk_default_reject
    mode tcp
    # This backend can be used to explicitly reject connections
    # that don't match any SNI rules.
    # A more explicit way to reject:
    tcp-request content reject
    # Alternatively, route to a generic error service or drop silently.
    # For silent drop, define no servers or use a blackhole server.

Remember to adjust IP addresses, ports, hostnames, and server names according to your specific environment. Also, ensure paths for chroot, stats socket, and errorfile match your HAProxy installation.

Testing the Configuration

The openssl s_client command-line tool is invaluable for testing SNI-based routing. To test connectivity to mqtt.example.com through HAProxy (assuming HAProxy is at your_haproxy_ip):

1
2
3
openssl s_client -connect your_haproxy_ip:443 \
                 -servername mqtt.example.com \
                 -brief

This command attempts a TLS connection to your_haproxy_ip:443 and passes mqtt.example.com as the SNI.

Key things to check in the output:

  • Successful Connection: Indicated by Verification: OK or similar, and session details.
  • Certificate Details: The certificate presented should be from the mqtt.example.com backend server, matching the SNI.
  • Protocol Handshake: After the TLS handshake, you might be able to send protocol-specific commands if you know them.

Repeat the test for each configured SNI hostname (e.g., xmpp.chat.org, imap.example.org) to ensure they are routed to the correct backends.

Logging SNI Information

To log the SNI value captured by HAProxy, you can customize the log format. See HAProxy Configuration Manual - log-format. First, capture the SNI value in the frontend:

1
2
3
4
frontend ft_secure_passthrough
    # ... other directives ...
    tcp-request content capture req.ssl_sni len 100
    # ...

Then, use capture.req.hdr(0) (or the specific capture slot) in your log-format string in the defaults or global section. This example log-format string might be too long for a single line in this document, but would be a single line in haproxy.cfg. For readability here, it’s wrapped:

1
2
3
4
5
6
defaults
    # Example log format including captured SNI (capture slot 0)
    # In haproxy.cfg, this would be a single long line.
    log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC \
%CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq {%hrl} \
SNI:%[capture.req.hdr(0)] %{+Q}r"

HAProxy versions 1.7+ also allow direct use of %{+Q}[req.ssl_sni] in the log format:

1
2
3
4
5
defaults
    # Simpler SNI logging for HAProxy 1.7+
    # In haproxy.cfg, this would be a single long line.
    log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC \
%CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq SNI:%[req.ssl_sni] %{+Q}r"

This allows you to see which SNI is being presented for each connection in your HAProxy logs.

Common Challenges and Pitfalls

  • Missing tcp-request inspect-delay: If HAProxy doesn’t wait long enough, it may not receive the full ClientHello and thus fail to extract SNI, leading to connections falling through to the default_backend.
  • Missing tcp-request content accept if { req_ssl_hello_type 1 }: Without this, HAProxy might try to parse SNI from non-ClientHello TCP packets, causing errors or misrouting.
  • Client Not Sending SNI: Older or misconfigured clients might not send SNI. These connections will be routed to the default_backend.
  • Incorrect SNI in ACLs: Typos or case-mismatches (if -i is not used) in hostnames in ACL definitions.
  • Backend Server Misconfiguration:
    • Servers not listening on the correct IP/port.
    • Servers not configured for SSL/TLS.
    • SSL/TLS certificates on backend servers not matching the SNI hostname, or being expired/invalid, causing client-side errors.
    • The specific non-HTTP application on the backend not functioning correctly.
  • Firewall Rules: Firewalls blocking traffic between clients and HAProxy, or between HAProxy and backend servers.
  • SSL/TLS Termination Attempted on HAProxy: Adding ssl crt to the bind line in the passthrough frontend will cause HAProxy to attempt termination, which conflicts with the passthrough goal.

Advanced Considerations

  • Encrypted SNI (ESNI) / Encrypted Client Hello (ECH): These emerging TLS features (ECH is specified in RFC 9520 which obsoletes earlier ESNI drafts) encrypt the SNI value itself. If clients use ECH widely, HAProxy (and other proxies relying on SNI inspection for passthrough) will not be able to read the SNI, breaking this routing method. This is a future consideration; currently, SNI is generally visible.
  • ALPN (Application-Layer Protocol Negotiation): Clients can also indicate the application protocol they wish to use (e.g., mqtt, xmpp-client) via the ALPN TLS extension (RFC 7301). HAProxy can inspect this using req.ssl_alpn and use it for routing decisions, often in conjunction with SNI.
    1
    2
    
    acl is_mqtt_protocol req.ssl_alpn -i mqtt
    use_backend bk_mqtt_specific if mqtt_traffic and is_mqtt_protocol
    
  • PROXY Protocol: To pass the original client’s IP address to backend servers (which is otherwise lost), enable the PROXY protocol on HAProxy for the server lines and ensure backend services support it.
    1
    2
    3
    4
    
    backend bk_mqtt_passthrough
        mode tcp
        # ...
        server mqtt_server1 10.0.1.10:8883 check send-proxy-v2
    

Conclusion

Configuring HAProxy for SSL/TLS passthrough with SNI-based backend selection provides a powerful and flexible way to route various encrypted non-HTTP protocols. By operating in mode tcp and correctly inspecting the SNI from the unencrypted ClientHello, HAProxy can direct traffic to appropriate backend services without needing to decrypt it, preserving end-to-end encryption and simplifying certificate management on the proxy.

This setup is ideal for environments hosting multiple distinct TLS-secured services like IoT platforms (MQTT), chat servers (XMPP), or secure email relays, all consolidated behind a single public-facing IP address and port. Careful attention to the tcp-request inspect-delay and tcp-request content accept directives, along with accurate ACLs and robust backend configurations, is key to a successful and secure deployment.