← Back to Blog

HAProxy SSL Termination Patterns for Multi-Domain Environments

January 2025

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 reload to 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.

← Back to Blog