HAProxy SSL Termination with Wildcard SSL Certificate

haproxylets-encryptload balancingsni

I am new to HAProxy and got most parts working as expected. The current setup is: If I add a new site to one of the balanced (behind the LB) servers, the certificate is issued and served by the Load Balancer.
So SSL Termination is working fine with regular Let's Encrypt certificates, but I have a limitation in this setup by the service I am using:

If I add a new site to a balanced server and want to use a wildcard *.wilddomain.com certificate, it is not issued by the Load Balancer, but by the balanced server (10.0.0.10). As LE validation is done over DNS, the wildcard certificate is valid and available on the balanced server now.

So now I have a Load Balancer with several "regular" LE certs which are used corretly, and a server behind which holds the wildcard certificate.

My question is: How can I set up HAProxy to passthrough to the wildcard certificate only for a specific domain (wilddomain.com) while serving all other certificates directly from the LB with SSL Termination.

My current config is this:

global
    log /dev/log        local0
    log /dev/log        local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    # Default SSL material locations
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private

    # Default ciphers to use on SSL-enabled listening sockets.
    # For more information, see ciphers(1SSL). This list is from:
    #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
    # An alternative list with additional directives can be obtained from
    #  https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
    ssl-default-bind-ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE->
    ssl-default-bind-options no-sslv3

defaults
    log global
    mode        http
    option      httplog
    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

# Default Let's Encrypt backend server used for renewals and requesting certificates
backend letsencrypt-backend
    server letsencrypt 127.0.0.1:8888

# Load balancer settings
frontend load-balancer
    bind *:80

    bind *:443 ssl crt /etc/ssl/domain1.com/domain1.com.pem crt /etc/ssl/domain2.com/domain1.com.pem

    redirect scheme https code 301 if !{ ssl_fc }

    # See if its an letsencrypt request
    acl letsencrypt-acl path_beg /.well-known/acme-challenge/
    use_backend letsencrypt-backend if letsencrypt-acl

    mode http
    default_backend webservers

# Backend webservers (the attached servers to the load balancer)
backend webservers
    balance roundrobin
    option forwardfor
    cookie SRVNAME insert
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    # Server www1
    server www1 10.0.0.10:80 weight 1 check
    # Server www2
    server www2 10.0.0.11:80 weight 1 check

EDIT I

I came a bit further by adding the following to the above config, but this produces "load-balancer/2: SSL handshake failure" in the HAProxy logs.

frontend wildcard_tcp
    bind *:443
    option tcplog
    mode tcp 

    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 } 

    acl is_wilddomain req_ssl_sni -m end wilddomain.com

    use_backend wildcard_server_tcp  if is_wilddomain

backend wildcard_server_tcp
    mode tcp 
    server ssl-wildcard-server 10.0.0.10:443

Is this a suitable and correct solution? Or is there a better / more performant one?
Would it be even possible to have a very basic backend server that is only responsible for the ssl-offload? So only for issuing, renewing and serving the certificates?

Thanks so much!

Best Answer

tl;dr this can be done configuring a TCP proxy listening all requests and using the SNI extension to either: 1) call a TCP backend which leaves the ssl-offload to the server, or 2) call a HAProxy's HTTP frontend that does the ssl-offload.


HAProxy can be configured to use distinct certificates for distinct domains in the same IP/port, hence in the same bind line, when performing a TLS handshake. This configuration can be fine tuned using the crt-list keyword in the bind line.

Such configuration however doesn't have an option to passthrough the ssl-offload to a backend server. A HAProxy frontend should be configured to either perform the ssl-offload, or should be configured as mode tcp and leave the ssl-offload to the backend.

In order to achieve mixed local and remote ssl-offload in the same IP/port, for distinct domains, another proxy should be added to the HAProxy configuration:

                                             +-----------------+
                                             |                 |
            +------+     (TCP request)       | wildcard server |
   O        |      | === *.wildcard.com ===> |                 |
  -|-   ==> | mode |                         +-----------------+
  / \       | tcp  | (local socket)  +-------------+
            |      | === others ===> |             |
            +------+                 | https front |
                                     | ssl-offload |
                                     |             |
                                     +-------------+
                                            |
                                            | (plain http request)
                                            |
                                            v
                                     +---------------+
                                     |               |
                                     | other servers |
                                     |               |
                                     +---------------+

The following snippet has the fronting TCP proxy and a local ssl-offload frontend. Note that this will consume twice the number of connections, tune global maxconn accordingly.

defaults
  timeout server 1s
  timeout client 1s
  timeout connect 1s
listen public
  mode tcp
  bind :443,:::443
  tcp-request inspect-delay 5s
  tcp-request content accept if { req.ssl_hello_type 1 }
  acl wildcard req.ssl_sni wildcard.local
  acl wildcard req.ssl_sni -m end .wildcard.local
  use_backend passthrough if wildcard
  server local_offload unix@/var/run/local.sock send-proxy-v2
backend passthrough
  mode tcp
  server ssl 10.0.0.10:443
listen local_offload
  mode http
  bind unix@/var/run/local.sock ssl crt /var/haproxy/crt.pem accept-proxy
  server plain0 10.0.0.10:80
  server plain1 10.0.0.11:80