HAProxy SSL Termination Patterns for Multi-Domain Environments
When running multiple domains through a single HAProxy instance, you need to handle SSL termination efficiently while routing requests to the correct backends. This article covers practical patterns for multi-domain SSL with HAProxy.
The Challenge
A typical multi-domain setup needs to:
- Terminate SSL for multiple domains with different certificates
- Route requests to different backends based on domain
- Handle certificate renewal without downtime
- Support both apex domains and subdomains
Pattern 1: Certificate Directory with SNI
The simplest approach is to put all certificates in a directory and let HAProxy select the right one based on SNI (Server Name Indication):
global
log stdout format raw local0
maxconn 4096
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11
defaults
log global
mode http
option httplog
timeout connect 5s
timeout client 50s
timeout server 50s
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
bind *:80
# Redirect HTTP to HTTPS
http-request redirect scheme https unless { ssl_fc }
# Route based on Host header
use_backend app1_backend if { hdr(host) -i app1.example.com }
use_backend app2_backend if { hdr(host) -i app2.example.com }
use_backend www_backend if { hdr(host) -i example.com } || { hdr(host) -i www.example.com }
default_backend default_backend
Certificate files in /etc/haproxy/certs/ should be named after the domain and contain both the certificate and private key:
# Combine cert and key for HAProxy
cat example.com.crt example.com.key > /etc/haproxy/certs/example.com.pem
cat app1.example.com.crt app1.example.com.key > /etc/haproxy/certs/app1.example.com.pem
Pattern 2: Explicit Certificate Mapping
For more control, use an explicit certificate map file:
frontend https_front
bind *:443 ssl crt-list /etc/haproxy/crt-list.txt alpn h2,http/1.1
The crt-list.txt file maps SNI patterns to certificates:
# /etc/haproxy/crt-list.txt
# Format: certificate_path [sni_filter] [options]
/etc/haproxy/certs/example.com.pem example.com
/etc/haproxy/certs/example.com.pem www.example.com
/etc/haproxy/certs/wildcard.example.com.pem *.example.com
/etc/haproxy/certs/app1.com.pem app1.com
/etc/haproxy/certs/app1.com.pem [*.app1.com]
This approach gives you explicit control over which certificate serves which domain and allows using wildcard certificates for subdomains.
Pattern 3: SNI-Based Backend Routing (Without Termination)
Sometimes you want HAProxy to route based on SNI but let backends handle SSL termination. This is useful for TCP passthrough:
frontend tcp_front
bind *:443
mode tcp
option tcplog
# Inspect SNI without terminating
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# Route based on SNI
use_backend app1_tcp if { req_ssl_sni -i app1.example.com }
use_backend app2_tcp if { req_ssl_sni -i app2.example.com }
default_backend default_tcp
backend app1_tcp
mode tcp
server app1 10.0.1.10:443
backend app2_tcp
mode tcp
server app2 10.0.1.20:443
Pattern 4: Mixed Mode - Terminate Some, Pass Others
In some architectures, you want to terminate SSL for some domains while passing others through:
# TCP frontend for pass-through domains
frontend tcp_front
bind *:443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# Pass through specific domains
use_backend legacy_passthrough if { req_ssl_sni -i legacy.example.com }
# Everything else goes to HTTP frontend for termination
default_backend haproxy_https
backend haproxy_https
mode tcp
server loopback 127.0.0.1:8443
# HTTPS frontend with termination
frontend https_front
bind 127.0.0.1:8443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
mode http
use_backend app1_backend if { hdr(host) -i app1.example.com }
default_backend default_backend
Certificate Renewal with Let's Encrypt
For automatic certificate renewal, combine HAProxy with certbot using the HTTP-01 challenge:
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
bind *:80
# ACME challenge - route to certbot
acl is_acme path_beg /.well-known/acme-challenge/
use_backend acme_backend if is_acme
# Redirect other HTTP to HTTPS
http-request redirect scheme https unless { ssl_fc } || is_acme
# Normal routing
use_backend app_backend if { hdr(host) -i app.example.com }
backend acme_backend
server certbot 127.0.0.1:8080
The renewal script updates certificates and reloads HAProxy:
#!/bin/bash
# /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
DOMAIN=$RENEWED_LINEAGE
CERT_DIR="/etc/haproxy/certs"
# Combine cert and key
cat "$DOMAIN/fullchain.pem" "$DOMAIN/privkey.pem" > "$CERT_DIR/$(basename $DOMAIN).pem"
# Reload HAProxy without dropping connections
systemctl reload haproxy
Testing Your Configuration
Verify SNI routing works correctly:
# Check which certificate is served
openssl s_client -connect example.com:443 -servername app1.example.com 2>/dev/null | \
openssl x509 -noout -subject
# Test with curl
curl -v --resolve app1.example.com:443:YOUR_HAPROXY_IP https://app1.example.com/
# Check HAProxy stats
echo "show stat" | socat stdio /var/run/haproxy.sock
Performance Considerations
For high-traffic multi-domain setups:
- SSL session caching - Reduces handshake overhead for returning clients
- OCSP stapling - Speeds up certificate validation
- Connection reuse - Keep-alive to backends
global
# SSL session cache (shared across all frontends)
tune.ssl.default-dh-param 2048
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
defaults
option http-keep-alive
timeout http-keep-alive 10s
Common Pitfalls
- Certificate order matters - Put the most specific certificates first in crt-list
- Missing intermediate certificates - Include the full chain in your .pem files
- File permissions - HAProxy needs read access to certificate files
- Reload vs restart - Use
reloadto avoid dropping connections
Conclusion
HAProxy's flexibility makes it well-suited for multi-domain SSL termination. The certificate directory approach works well for simpler setups, while explicit crt-list mapping gives you fine-grained control. For zero-downtime certificate renewal, the ACME challenge routing pattern integrates smoothly with Let's Encrypt.