Switch Frontends Based on TLS Version

I have an application behind haproxy with two different clients:

client 1 uses TLS 1.2/1.3 to connect to backend /photo-app/
client 2 can only use TLS 1.0 to connect to backend /photo-app/scan/

In order to support both clients, the application is forced to allow TLSv1 connections. This shows up as a vulnerability from standard website scans (using tools such as testssl.sh or SSL Labs). I am stuck with client 2 for the time being and I cannot change the URL or protocols/ciphers they use to connect. This is a legacy integration I am working to retire.

My current haproxy frontend implementation does properly reject client 1 non-secure requests, but that only happens after SSL termination. The goal is to also show that /photo-app/ is not vulnerable to TLS 1.1 and below.

I found a post that solves a related problem here. Unfortunately, I am unable to distinguish the two clients based on SNI, since they are the same.

This is what I have so far, but I have been unable to make client 2 requests work when trying to route on TLS version.

global
  log /dev/log local0 debug

  ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
  ssl-default-bind-options no-sslv3 no-tls-tickets

  # https://datatracker.ietf.org/doc/html/rfc7919
  ssl-dh-param-file /usr/local/etc/haproxy/ffdhe2048.dhe

defaults
  log global
  mode http
  balance roundrobin
  option dontlognull
  timeout http-keep-alive 5s
  timeout http-request 10s
  timeout client 10s
  timeout connect 5s
  timeout server 10m
  timeout check 5s
  default-server inter 30s
  default-server rise 2
  default-server fall 2
  compression algo gzip
  compression type text/html text/css text/javascript application/javascript


frontend http
  bind *:80
  http-request redirect scheme https if !{ ssl_fc }


frontend https
  bind *:443
  mode tcp
  option tcplog

  tcp-request inspect-delay 5s
  tcp-request content accept if { req.ssl_hello_type 1 }

  use_backend recirc_client2 if { WHAT-GOES-HERE? }
  default_backend recirc_default


backend recirc_client2
  mode tcp
  server loopback-for-tls abns@haproxy-client2 send-proxy-v2


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


frontend client2
  option httpslog
  option forwardfor

  bind abns@haproxy-client2 accept-proxy ssl crt /usr/local/etc/haproxy/photo-app.net.pem 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:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA

  acl url-photoapp-scan path_beg /photo-app/scan

  http-request deny if !url-photoapp-scan
  http-request add-header X-Forwarded-Proto https

  default_backend photoapp


frontend default
  option httpslog
  option forwardfor

  bind abns@haproxy-default accept-proxy ssl crt /usr/local/etc/haproxy/photo-app.net.pem 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:DHE-RSA-CHACHA20-POLY1305

  acl is-supported-tls ssl_fc_protocol TLSv1.2 TLSv1.3
  acl url-photoapp-scan path_beg /photo-app/scan
  acl url-photoapp path_beg /photo-app

  http-request deny if !is-supported-tls !url-photoapp-scan
  http-request add-header X-Forwarded-Proto https

  http-response set-header Strict-Transport-Security "max-age=63072000"

  default_backend photoapp


backend photoapp
  option httpchk GET /photo-app/health-check/
  server 1 10.72.100.1:8100 check cookie 1
  server 2 10.72.100.2:8100 check cookie 2
  cookie JSESSIONID prefix nocache
HAProxy version 2.9.12-1086254 2024/11/08 - https://haproxy.org/
Status: stable branch - will stop receiving fixes around Q1 2025.
Known bugs: http://www.haproxy.org/bugs/bugs-2.9.12.html
Running on: Linux 5.15.0-124-generic #134-Ubuntu SMP Fri Sep 27 20:20:17 UTC 2024 x86_64
Build options :
  TARGET  = linux-glibc
  CPU     = generic
  CC      = cc
  CFLAGS  = -O2 -g -Wall -Wextra -Wundef -Wdeclaration-after-statement -Wfatal-errors -Wtype-limits -Wshift-negative-value -Wshift-overflow=2 -Wduplicated-cond -Wnull-dereference -fwrapv -Wno-address-of-packed-member -Wno-unused-label -Wno-sign-compare -Wno-unused-parameter -Wno-clobbered -Wno-missing-field-initializers -Wno-cast-function-type -Wno-string-plus-int -Wno-atomic-alignment
  OPTIONS = USE_GETADDRINFO=1 USE_OPENSSL=1 USE_LUA=1 USE_PROMEX=1 USE_PCRE2=1 USE_PCRE2_JIT=1
  DEBUG   = -DDEBUG_STRICT -DDEBUG_MEMORY_POOLS

Feature list : -51DEGREES +ACCEPT4 +BACKTRACE -CLOSEFROM +CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ENGINE +EPOLL -EVPORTS +GETADDRINFO -KQUEUE -LIBATOMIC +LIBCRYPT +LINUX_CAP +LINUX_SPLICE +LINUX_TPROXY +LUA +MATH -MEMORY_PROFILING +NETFILTER +NS -OBSOLETE_LINKER +OPENSSL -OPENSSL_AWSLC -OPENSSL_WOLFSSL -OT -PCRE +PCRE2 +PCRE2_JIT -PCRE_JIT +POLL +PRCTL -PROCCTL +PROMEX -PTHREAD_EMULATION -QUIC -QUIC_OPENSSL_COMPAT +RT +SHM_OPEN +SLZ +SSL -STATIC_PCRE -STATIC_PCRE2 -SYSTEMD +TFO +THREAD +THREAD_DUMP +TPROXY -WURFL -ZLIB

Default settings :
  bufsize = 16384, maxrewrite = 1024, maxpollevents = 200

Built with multi-threading support (MAX_TGROUPS=16, MAX_THREADS=256, default=2).
Built with OpenSSL version : OpenSSL 3.0.15 3 Sep 2024
Running on OpenSSL version : OpenSSL 3.0.15 3 Sep 2024
OpenSSL library supports TLS extensions : yes
OpenSSL library supports SNI : yes
OpenSSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with Lua version : Lua 5.4.4
Built with the Prometheus exporter as a service
Built with network namespace support.
Built with libslz for stateless compression.
Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using: IP_TRANSPARENT IPV6_TRANSPARENT IP_FREEBIND
Built with PCRE2 version : 10.42 2022-12-11
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with gcc compiler version 12.2.0

Available polling systems :
      epoll : pref=300,  test result OK
       poll : pref=200,  test result OK
     select : pref=150,  test result OK
Total: 3 (3 usable), will use epoll.

Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto' keyword)
         h2 : mode=HTTP  side=FE|BE  mux=H2    flags=HTX|HOL_RISK|NO_UPG
       fcgi : mode=HTTP  side=BE     mux=FCGI  flags=HTX|HOL_RISK|NO_UPG
  <default> : mode=HTTP  side=FE|BE  mux=H1    flags=HTX
         h1 : mode=HTTP  side=FE|BE  mux=H1    flags=HTX|NO_UPG
  <default> : mode=TCP   side=FE|BE  mux=PASS  flags=
       none : mode=TCP   side=FE|BE  mux=PASS  flags=NO_UPG

Available services : prometheus-exporter
Available filters :
	[BWLIM] bwlim-in
	[BWLIM] bwlim-out
	[CACHE] cache
	[COMP] compression
	[FCGI] fcgi-app
	[SPOE] spoe
	[TRACE] trace

If you route a TLSv1.0 only client to a TLSv1.0 compatible SSL termination point, this means that tools like testssl.sh and SSL Labs will show that you have enabled TLSv1.0, because that is how they test TLSv1.0 support: by trying to connect with TLSv1.0.

If you want keep TLSv1.0 enabled for a specific client and you cannot do it via hostname/SNI, then you need to do it via hardcoded IP address.

You cannot access encrypted data like URI or paths before decrypting the SSL part.

1 Like

Thank you Lukas. Given a list of client 2 IP addresses, how would I test for that in the TCP frontend?

You can make an ACL matching IP addresses and then refer to that ACL when making routing decisions:

acl client1 src 192.0.2.35
acl client1 src 198.51.100.99

use_backend client1backend if client1
1 Like

Oh crud, that was easy. I really do appreciate the help as my mind is a bit blown from looking at this for so long.

1 Like