SSL Termination with exception for a specific domain Wildcard SSL-Certificate request

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

I ended in a very frustrating trial & error loop and don’t get further. So I really appriciate your answers :wink:

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?

Please provide both the complete configuration, so I can see the rest of the configuration as well as the actual log line.

Hi @lukastribus,

happy to share that with you! So my full config is:

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

# !!! UNTIL HERE IS AUTOCONFIGURED BY THE SERVICE USED
# !!! FROM HERE I EXTEND THE AUTO-CONFIGURATION

# Add global settings
global
    tune.ssl.default-dh-param 2048

# Pass SSL requests for *.ouun.io to server ouun-certs
frontend wildcard_tcp
    mode tcp
    bind :443,:::443
    option tcplog

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

    acl is_wilddomain req.ssl_sni wilddomain.com
    acl is_wilddomain req.ssl_sni -m end .wilddomain.com

    # Offload wilddomain.com TCP requests
    use_backend wildcard_server_tcp if is_wilddomain

    # Route to default config otherwise
    default_backend webservers

backend wildcard_server_tcp
    mode tcp
    server www1 10.0.0.11:443

And my logs:

Oct 13 10:22:55 restart haproxy[50178]: 208.100.xx.xxx:49280 [13/Oct/2020:10:22:55.967] load-balancer~ webservers/www1 1/0/1/1/3 404 274 - - --NI 10/1/9/0/0 0/0 "GET /nice%20ports%2C/Tri%6Eity.txt%2ebak HTTP/1.0"
Oct 13 10:24:45 restart haproxy[50178]: 208.100.xx.xxx:57988 [13/Oct/2020:10:24:45.466] load-balancer~ webservers/www1 1/0/1/36/38 302 258 - - --NI 10/1/9/0/0 0/0 "GET /nmaplowercheck1602577485 HTTP/1.1"
Oct 13 10:24:46 restart haproxy[50178]: 208.100.xx.xxx:58498 [13/Oct/2020:10:24:45.986] load-balancer~ webservers/www1 0/0/1/46/47 302 258 - - --NI 12/3/9/0/0 0/0 "GET /evox/about HTTP/1.1"
Oct 13 10:24:46 restart haproxy[50178]: 208.100.xx.xxx:58642 [13/Oct/2020:10:24:46.139] load-balancer~ webservers/www1 0/0/0/28/28 302 258 - - --NI 11/2/9/0/0 0/0 "POST /sdk HTTP/1.1"
Oct 13 10:24:46 restart haproxy[50178]: 208.100.xx.xxx:58710 [13/Oct/2020:10:24:46.242] load-balancer~ webservers/www1 0/0/1/35/36 302 258 - - --NI 10/1/9/0/0 0/0 "GET /HNAP1 HTTP/1.1"
Oct 13 10:24:46 restart haproxy[50178]: 208.100.xx.xxx:59378 [13/Oct/2020:10:24:46.699] load-balancer/2: SSL handshake failure
Oct 13 10:24:47 restart haproxy[50178]: 208.100.xx.xxx:59554 [13/Oct/2020:10:24:47.208] load-balancer~ webservers/www1 0/0/0/29/29 302 258 - - --NI 10/1/9/0/0 0/0 "GET / HTTP/1.1"
Oct 13 10:24:47 restart haproxy[50178]: 208.100.xx.xxx:59986 [13/Oct/2020:10:24:47.918] load-balancer~ webservers/www1 0/0/1/20/21 302 258 - - --NI 10/1/9/0/0 0/0 "GET / HTTP/1.1"
Oct 13 10:28:38 restart haproxy[50178]: 116.203.xx.xx:38220 [13/Oct/2020:10:28:38.536] load-balancer~ webservers/www1 1/0/1/0/2 301 359 - - --NI 11/1/9/0/0 0/0 "GET /wp-json/wp/v2/types/post?context=edit HTTP/1.1"
Oct 13 10:29:04 restart haproxy[50178]: 209.17.xx.xxx:38415 [13/Oct/2020:10:29:04.296] load-balancer~ webservers/www1 0/0/1/1/2 301 327 - - --NI 11/1/9/0/0 0/0 "GET / HTTP/1.1"
Oct 13 10:31:34 restart haproxy[50178]: 5.126.xxx.xxx:12020 [13/Oct/2020:10:31:34.212] load-balancer/2: SSL handshake failure

Thank you and knd regards

You are binding twice to port 443, you cannot do that. Your OS will load-balance between the two frontend which is absolutely NOT what you want.

You need a single frontend on port 443, in TCP mode, which is looking at the SNI value for routing decision making. For domains that you want to terminate locally, you need to define a backend that reconnects to a SSL terminating frontend with your configuration.

So for example:

  • move the frontend load-balancer from port *:443 to 127.0.0.1:1443
  • create a backend in tcp mode to reconnect to 127.0.0.1:1443
  • use that backend as a default_backend in wildcard_tcp, as opposed to webservers

Also see this example (slightly different use-case):

Thank you @lukastribus, I will check that out in more detail. But the first issue I face is, that I am using a service for HAProxy where I am not able to modify the main part of the config but can only extend it. Please see the following comment in my config above:

# !!! UNTIL HERE IS AUTOCONFIGURED BY THE SERVICE USED
# !!! FROM HERE I EXTEND THE AUTO-CONFIGURATION

So I already did a research but was not able to figure out, whether it is possible to “overwrite” parts of the non-editable config in my extended config. No idea how to follow your recommendation otherwise to

THX!

You certainly cannot reach your goal when you are not in control of the configuration.