Force SSL renegotiation on subdomain change using wildcard certificate

Hi,

I’m using haproxy as an SSL terminator and SNI based service selector for my family server. Some of the subdomains use client side certificate, some of them not. Some of them are TCP, others are HTTP. For me haproxy is a convenient solution for SSL termination, authentication and even HTTP/2 support for my dummy embedded servers, alarm system, openvpn (over tls) and nextcloud instance. (A lot of things, except load balancing :slight_smile: )

My problem with switching subdomain in a browser from an unauthenticated section to a restricted section. I use Let’s encrypt wildcard certificate for my domain, so the browser would like to use the same ssl session with the same SNI for all the subdomains. Only the HTTPS services are affected, so I can check the HOST field in the HTTP header. Now I know, that the browser want to reach a restricted subdomain and drop and error message. The only way to reach the restricted subdomain is to restart the browser.

Is it possible to close the SSL session (see close-session-backend in my config) to force the browser for a new SSL negotiation (with client certificate)? I don’t want to use the verify optional solution at bind, as I don’t want the browser to popup a window for client side certificate where I don’t need it. After all when I don’t give a certificate there, I don’t think it will prompt again when I switch subdomains.

I was thinking about using SNIs in the crt-list.txt, but I don’t think it would work, as the same wildcard certificate will be used for every line and the browser will know nothing about this trick. Will haproxy handle these lines in the crt-list.txt as different ssl sessions?
I don’t want to use two or more certificates if possible, wildcard is a very flexible solution for me.

Thank you for your help!

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

        # Default ciphers to use on SSL-enabled listening sockets.
        ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets


        tune.ssl.default-dh-param 2048
        maxconn 2048

defaults
        log     global
        mode    tcp
        option  tcplog
        option  logasap

        timeout connect  10000
        timeout client  300000
        timeout server  300000
        source 0.0.0.0 usesrc clientip
        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

##############################################
# http -> https redirection
##############################################
frontend http-frontend
        mode http
        option httplog
        bind 192.168.10.52:80
        redirect scheme https code 301 if !{ ssl_fc }

##############################################
# Switch between subdomains by sni
##############################################
frontend mode-switch
        # option tcplog
        bind 192.168.10.52:443
        tcp-request inspect-delay 5s
        tcp-request content accept if { req.ssl_hello_type 1 }
        use_backend verif-none-tcp if { req.ssl_sni -i ovpn.mydomain.com }
        use_backend verif-required-tcp if { req.ssl_sni -i tcpservice1.mydomain.com }
        use_backend verif-none-error if !{ req.ssl_sni -m dom mydomain.com }
        use_backend verif-none if { req.ssl_sni -i mydomain.com } || { req.ssl_sni -i www.mydomain.com } || { req.ssl_sni -i other.mydomain.com }
        default_backend verif-required

backend verif-none
        server recir abns@haproxy-normal send-proxy-v2

backend verif-required
        server recir abns@haproxy-clientcert send-proxy-v2

backend verif-none-tcp
        server recir abns@haproxy-tcp-normal send-proxy-v2

backend verif-required-tcp
        server recir abns@haproxy-tcp-clientcert send-proxy-v2

backend verif-none-error
        server recir abns@haproxy-error send-proxy-v2

##############################################
# HTTPS subdomains without client cert.
##############################################
# https://mydomain.com
# https://www.mydomain.com
# https://other.mydomain.com
##############################################
frontend fe-ssl-normal
        mode http
        bind abns@haproxy-normal accept-proxy ssl crt-list /etc/haproxy/certs/crt-list.txt alpn h2,http/1.1 no-tls-tickets strict-sni
        use_backend https-backend if { hdr(host) -i mydomain.com } || { hdr(host) -i www.mydomain.com } || { hdr(host) -i other.mydomain.com }

        # Wants to reach a restricted subdomain with wrong SNI
        default_backend close-session-backend

backend https-backend
        mode http
        option forwardfor
        option http-server-close
        http-request set-header X-Forwarded-Port %[dst_port]
        http-request add-header X-Forwarded-Proto https
        http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains"
        server apache2-https 192.168.10.52:81

##############################################
# HTTPS subdomains with client cert.
##############################################
# https://*.mydomain.com
##############################################
frontend fe-ssl-clientcert
        mode http
        bind abns@haproxy-clientcert accept-proxy ssl crt-list /etc/haproxy/certs/crt-list.txt ca-file /etc/haproxy/certs/https_client_certs/ca.crt verify required crl-file /etc/haproxy/certs/https_client_certs/root_crl.pem alpn h2,http/1.1 no-tls-tickets strict-sni
        use_backend alarm-backend if { ssl_fc_sni -i alarm.mydomain.com } { ssl_c_s_dn(CN) -m reg ^(user1|user2)$ }
        use_backend gitlist-backend if { ssl_fc_sni -i gitlist.mydomain.com } { ssl_c_s_dn(CN) -m reg ^(user1)$ }
        default_backend access-denied-backend


backend alarm-backend
        mode http
        source 192.168.10.52
        server alarm-http 192.168.10.51:8000

backend gitlist-backend
        mode http
        option forwardfor
        option http-server-close
        http-request set-header X-Forwarded-Port %[dst_port]
        http-request add-header X-Forwarded-Proto https
        http-response set-header Strict-Transport-Security "max-age=15552000; includeSubDomains"
        server apache2-https 192.168.10.52:81

##############################################
# TLS / TCP subdomains without client cert.
##############################################
# https://ovpn.mydomain.com -- OpenVPN
##############################################
frontend fe-ssl-tcp-normal
        mode tcp
        bind abns@haproxy-tcp-normal accept-proxy ssl crt-list /etc/haproxy/certs/crt-list.txt
        use_backend ovpn-backend if { ssl_fc_sni -i ovpn.mydomain.com }

backend ovpn-backend
        mode tcp
        server ovpn 192.168.10.52:1122

##############################################
# TLS / TCP subdomains with client cert.
##############################################
# https://tcpservice1.mydomain.com
##############################################
frontend fe-ssl-tcp-clientcert
        mode tcp
        bind abns@haproxy-tcp-clientcert accept-proxy ssl crt-list /etc/haproxy/certs/crt-list.txt ca-file /etc/haproxy/certs/https_client_certs/ca.crt verify required crl-file /etc/haproxy/certs/https_client_certs/root_crl.pem
        use_backend tcpservice1-backend if { ssl_fc_sni -i tcpservice1.mydomain.com } { ssl_c_s_dn(CN) -m reg ^(user1|user2|user3|user4)$ }

backend tcpservice1-backend
        mode tcp
        server tcpservice1 192.168.10.52:54321

##############################################
# Error backends
##############################################
frontend fe-ssl-error
        mode http
        option httplog
        bind abns@haproxy-error accept-proxy ssl crt-list /etc/haproxy/certs/crt-list.txt alpn h2,http/1.1
        use_backend error-backend

backend access-denied-backend
        mode http
        errorfile 503 /etc/haproxy/err_access_denied.http

backend error-backend
        mode http
        errorfile 503 /etc/haproxy/err_no_sni.http

backend close-session-backend
        mode http
#       option forceclose
        errorfile 503 /etc/haproxy/err_access_denied.http
#       http-request redirect code 301 location https://%[hdr(host)]%[capture.req.uri]

Afaik the only product that can do this is Apache (triggering on demand TLS renegotiation, even based on different directories).

But this is not a solution going forward: with TLS v1.3, there is no renegotiation anymore.

You will have to find another solution. I understand using the wildcard certificate is convenient, but how about using two wildcard certificates, one for client cert auth, and one for everything else.

Like:
*.mydomain.com → everything without client cert auth
*.auth.mydomain.com → moving alarm and gitlist into this subdomain

Those two wildcard certificates do not overlap at all, as the wildcard only matches a single DNS label.

Thank you! Then I will do the magic with the certificates.

Note for the future: it is possible to request a client side certificate in an existing session in TLS v1.3 by openssl. According to the current github source code, this feature is not yet supported by haproxy.

See: TLS1.3 - OpenSSLWiki

Renegotiation

TLSv1.3 does not have renegotiation so calls to SSL_renegotiate() or SSL_renegotiate_abbreviated() will immediately fail if invoked on a connection that has negotiated TLSv1.3.

A common use case for renegotiation is to update the connection keys. The function SSL_key_update() can be used for this purpose in TLSv1.3 (see here for further details).

Another use case is to request a certificate from the client. This can be achieved by using the SSL_verify_client_post_handshake() function in TLSv1.3 (see here for further details).

Great, good to know. I’m not sure how easy it is to glue this all together, but it is something worth keeping in mind.