How to set ssl verify client for specific domain name

Hi, all

I have two domain name test1 and test2
test1 needs to verify client certificate,
test2 is a normal https website

here’s the config for test1, but I don’t know how to merge test2 to it becase test2 does not need to verify client certificate, seems ‘verify required’ is a global option, how can I just let test1 to verify client certificate? Thanks for the help (I’m new to HAProxy, please correct me if anything wrong in my config, thanks a lot.).

frontend http_in
        bind *:80
        bind *:443 ssl crt /etc/ssl/certsforhaproxy/test1.pem crt /etc/ssl/certsforhaproxy/test2.pem ca-file /etc/ssl/certsforhaproxy/ca.pem verify required
        redirect scheme https if !{ ssl_fc }
        acs host_test1 hdr_beg(host) test1.demo.com
        acs host_test2 hdr_beg(host) test2.demo.com
        use_backend test1_back if host_test1
        use_backend test2_back if host_test2

backend test1_back
        mode http
        default-server inter 2s fall 2 rise 2
        server node1 10.10.0.1:1234 check port 1234
        server node2 10.10.0.2:1234 check port 1234
        server node3 10.10.0.3:1234 check port 1234

backend test2_back
        mode http
        default-server inter 2s fall 2 rise 2
        server node1 10.10.0.1:2345 check port 2345
        server node2 10.10.0.2:2345 check port 2345
        server node3 10.10.0.3:2345 check port 2345

Only thing I can think of right now is to set the verify to optional so clients can connect with or without the client certificate and then restrict access using an ACL like so:

acl restricted hdr(host) -i somedomain.com
http-request deny if restricted ! { ssl_c_used }

There is no simple way to do this, unfortunately.

Use a TCP frontend withouth SSL termination, SNI route to different backends that recirculate to traffic to dedicated SSL frontends with different configurations.

Something like:

frontend port443
    bind :443
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    use_backend recir_clientcertenabled if { req_ssl_sni -i test1.demo.com }
    default_backend recir_default

backend recir_clientcertenabled
    server loopback-for-tls abns@haproxy-clientcert send-proxy-v2
backend recir_default
    server loopback-for-tls abns@haproxy-default send-proxy-v2

frontend fe-ssl-clientcert
    mode http
    bind abns@haproxy-clientcert accept-proxy ssl crt /etc/ssl/certsforhaproxy/test1.pem crt ca-file /etc/ssl/certsforhaproxy/ca.pem verify required
frontend fe-ssl-default
    mode http
    bind abns@haproxy-default accept-proxy ssl crt /etc/ssl/certsforhaproxy/test2.pem crt
1 Like

Improvement to my answer…

Set the verify option on the bind line to “optional” and use the following ACL:

acl restricted hdr(host) -i somedomain.com
http-request deny if restricted !{ ssl_c_used 1 } || restricted !{ ssl_c_verify 0 }

The above simply says if the header host matches a specific domain deny request unless the client has provided a certificate and that certificate was verified.

You can improve on this by adding specific error pages, example:

redirect location /certmisiing.html if restricted !{ ssl_c_used 1 }
redirect location /certexpired.html if restricted { ssl_c_verify 10 }
redirect location /certrevoked.html if restricted { ssl_c_verify 23 }
redirect location /othererrors.html if restricted !{ ssl_c_verify 0 } 

I even wrote a Blog on the subject, hope that helps: https://www.loadbalancer.org/blog/client-certificate-authentication-with-haproxy/

“verify optional” has a rather important disadvantage: it changes the SSL handshake, leading to different browser behavior for all of the websites.

For example, if you have a browser with a client certificate installed for a unrelated domain, and haproxy handles test1.demo.com and test2.demo.com, the browser will ask you for both test1.demo.com and test2.demo.com if and which client certificate you wanna send.

While this is expected behavior for test1.demo.com it isn’t for test2.demo.com.

By SNI routing to different SSL termination endpoints like suggest above we avoid this problem. We also have a “proper” SSL handshake failure with “verify required”, instead of failure on the HTTP layer.

Thanks Lukas, I found this also in my testing, I was very much playing and reverse engineering how it works learning lots of new stuff for the first time. Yesterday I referenced your better solution with SNI in my Blog, I hope that’s okay, I gave you credit and linked it back here.

1 Like

Hi,

I tried to get lukas’ solution running, but with no success

frontend https-switch
    bind *:444
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    #use_backend recir_clientcertenabled if { req_ssl_sni -i www.mydomain.de }
    default_backend recir_default

backend recir_clientcertenabled
    server loopback-for-tls abns@haproxy-clientcert send-proxy-v2
backend recir_default
    server loopback-for-tls abns@haproxy-default send-proxy-v2

frontend https-cert-required
    bind abns@haproxy-clientcert accept-proxy ssl crt /etc/ssl/private/mydomain.de.pem ca-file /etc/ssl/private/client-authentication.pem verify required
    # HSTS (15768000 seconds = 6 months)
    http-response set-header Strict-Transport-Security max-age=15768000
    mode http
    default_backend nodes-cert-required

frontend https-default
    bind abns@haproxy-default ssl accept-proxy crt /etc/ssl/private/mydomain.de.pem crt /etc/ssl/private/mydomain2.de.pem
    # HSTS (15768000 seconds = 6 months)
    http-response set-header Strict-Transport-Security max-age=15768000
    mode http
    default_backend nodes-https

But everytime I try to open https://www.mydomain.de:444 I receive an ssl error in the browser:
SSL_ERROR_RX_RECORD_TOO_LONG

In the log file I could not find any hint too:

Mar 29 17:00:32 sql2 haproxy[13206]: 95.88.243.138:50938 [29/Mar/2018:17:00:32.101] https-switch https-switch/<NOSRV> -1/-1/-1/-1/0 400 188 - - PR-- 22/0/0/0/0 0/0 "<BADREQ>"

Has someone made a running configuration?
@AaronWest: can you give us your blog reference?

Thanks!

Marc

You need “mode tcp” in the default section, or in the first 3 sections in my proposal. TCP mode is required for SNI switching.

Thank you!

That was the missing part. In my default section was mode http.

I was trying something very similar but path based ssl handshake. So far what I read is it is not possible with HAProxy. In this example, we have to use tcp mode. But in order to check the path, we need http mode.

    frontend https-switch
        bind *:8888
        tcp-request inspect-delay 5s
        tcp-request content accept if { req_ssl_hello_type 1 }
        acl auth_request path_end -i /auth   # this will always return false
        use_backend recir_clientcertenabled if auth_request
        default_backend recir_default

Well. It was very easy to implement mine.

listen http-in
    bind *:8888 ssl crt /etc/ssl/certs/haproxy.pem verify optional ca-file /etc/ssl/certs/rootCA.crt
    http-request set-header SSL_CLIENT_CERT %[ssl_c_der,base64]
    http-request deny if { path_end /auth } !{ ssl_c_used }
    #redirect scheme https code 301 if !{ ssl_fc }
    server server1 host.docker.internal:8443 ssl verify none

I only want request ending at /auth to be asked for ssl exchange and others should just pass.

@lukastribus , hi there, this is an old post but I hope you can help me a bit :stuck_out_tongue:

First, you mentioned that it needs to be mode tcp but my current env uses HTTP.
I have tried a few changes and the check had errors all over the place. Even the log format was not being accepted anymore.

Second, our CDN manages our certificate so I would need to contact them to provide the files instead of using the self-certificate one.

frontend HTTPS
    maxconn 1000
    bind 0.0.0.0:443 ssl crt SELF_CERTIFIED_CERTIFICATE no-sslv3
    option httplog
    mode  http

    option http-server-close
    option forwardfor except 127.0.0.0/8
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Port 443
    capture request header      X-Forwarded-For  len 200
    capture request header      Host             len 100
    capture request header      Referrer         len 64
    capture request header      Content-Length   len 10
    capture request header      User-Agent       len 256
    capture cookie              JSESSIONID       len 43
    log-format %ci:%cp\ [%t]\ %f\ %b/%s\ %Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ "%r"\ %hr\ %sslv
         
    default_backend HTTPS 

The idea is to deny/reject any HTTPS access that is not coming from our CDN.
Right now, someone can access our website via the LB IP outside our CDN and WAF.
I am not expert with the HaProxy so I am trying to find a solution that I can comprehend.

Thank you so much.

This thread is totally unrelated with the questions you have, let’s move the conversation to your actual thread, or open a new one if the topic is different, thanks.

1 Like

@lukastribus I tride the config but with no success if I use the config where no SSL verification is needed everythings works well.
But if I want to use the https-client-ca-in route I get this error in my Browser and got never ask for an certificate:
ERR_BAD_SSL_CLIENT_AUTH_CERT

in the haproxy log I only see this:
Jun 25 19:20:02 haproxy haproxy[6435]: *.*.*.*:51812 [25/Jun/2023:19:20:02.522] http-in recir_clientcertenabled/loopback-for-tls 1/0/25 4777 SD 1/1/0/0/0 0/0
Jun 25 19:20:41 haproxy haproxy[6435]: *.*.*.*:51854 [25/Jun/2023:19:20:41.823] https-client-ca-in/1: SSL handshake failure (error:00000000:lib(0)::reason(0))
Jun 25 19:20:41 haproxy haproxy[6435]: *.*.*.*:51854 [25/Jun/2023:19:20:41.822] http-in recir_clientcertenabled/loopback-for-tls 1/0/19 4753 -- 1/1/0/0/0 0/0
Jun 25 19:20:42 haproxy haproxy[6435]: *.*.*.*:51858 [25/Jun/2023:19:20:42.165] https-client-ca-in/1: SSL handshake failure (error:0A0000C7:SSL routines::peer did not return a certificate)
Jun 25 19:20:42 haproxy haproxy[6435]: *.*.*.*:51858 [25/Jun/2023:19:20:42.164] http-in recir_clientcertenabled/loopback-for-tls 1/0/23 4777 SD 1/1/0/0/0 0/0

Im new to HAProxy, i hope that someone can help me.

hear is my configuration:

frontend http-in
        bind :443
        mode tcp
        tcp-request inspect-delay 5s
        tcp-request content accept if { req_ssl_hello_type 1 }
        use_backend recir_clientcertenabled if { req_ssl_sni -i test.example.de }
        default_backend recir_default

backend recir_clientcertenabled
        mode tcp
        server loopback-for-tls abns@haproxy-cert send-proxy-v2

backend recir_default
        mode tcp
        server loopback-for-tls abns@haproxy-default send-proxy-v2

frontend https-client-ca-in
        # ssl witch client cert
        bind abns@haproxy-cert accept-proxy ssl crt /etc/letsencrypt/live/example.de/example.com.pem ca-file /etc/ssl/ca.crt verify required crl-file /etc/ssl/ca.crl alpn h2,http/1.1

        # HSTS (63072000 seconds)
        http-response set-header Strict-Transport-Security max-age=63072000

        # host ermitteln
        use_backend ansible_out if { req_ssl_sni -i test.example.de }

frontend https-in
        # ssl mit client cert
        bind abns@haproxy-default accept-proxy ssl crt /etc/letsencrypt/live/example.de/example.com.pem alpn h2,http/1.1

        # HSTS (63072000 seconds)
        http-response set-header Strict-Transport-Security max-age=63072000
        # host ermitteln
        use_backend ansible_out if { req_ssl_sni -i test.example.de }

backend ansible_out
        http-request set-header         X-CLIENT-IP %[src]
        http-request set-header         X-Forwarded-Proto https
        server ansible 1.1.1.1:3000 check