Hi there,
I am sure I am doing something horribly wrong, but I just cannot find out what. I am trying to disable X25519MLKEM768 key exchanges for the backend connections to reduce the overhead of the connection. As such I am running with the following configuration on haproxy docker images haproxytech/haproxy:debian-3.4.5 as well as haproxy:3.4.5:
global
ssl-default-server-curves secp384r1
ssl-default-server-options ssl-min-ver TLSv1.3 no-tls-tickets
defaults
mode http
log global
option httplog
option dontlognull
timeout client 1s
timeout connect 1s
timeout server 1s
frontend main
bind *:8080
default_backend app
frontend test
bind *:4040
http-request return status 200 content-type text/plain string OK\n
backend app
default-server ssl verify none no-sni-auto no-check-sni-auto curves secp384r1
server test1 127.0.0.1:4040
ssl-default-server-curves secp384r1 is used on purpose for demonstration purposes. Now when connecting via HTTP to port 8080 haproxy tries to make a SSL connection to port 4040. It obviously fails because the backend is not SSL aware but the important part is in the “Client Hello” TLS message that haproxy sends:
As you can see the key_share still uses X25519MLKEM768. No matter what I try, I cannot get haproxy to send a non pqc aware key exchange.
Any ideas what I am doing wrong?
Thanks,
Florian
Uff, it seems to work if I put curves … on the server line, but not when used in default-server or the global options. Sounds like a bug?
Reported and fixed on Github:
opened 07:26PM - 25 Sep 26 UTC
type: bug
status: needs-triage
### Detailed Description of the Problem
I am tcpdumping haproxy connections to … backend servers and tcpdump shows:
<img width="1452" height="683" alt="Image" src="https://github.com/user-attachments/assets/835a6b60-628f-492c-ac1e-05a40b561e72" />
As you can see, haproxy tries a pqc key exchange when it should (given my configuration) use `secp384r1`.
### Expected Behavior
Given my configuration I would have expected `secp384r1` as key exchange method.
### Steps to Reproduce the Behavior
1. Run haproxy with the configuration pasted below, ie something like `podman run --net=host --rm -it -v /tmp/haproxy.cfg:/etc/haproxy/haproxy.cfg:z haproxytech/haproxy:debian-3.4`
2. Open wireshark and dump traffic to localhost port 4040
3. Do a curl/browser HTTP request to port 8080 so haproxy will trigger a connection to the backend
4. Observe the wrong key exchange in the "Client Hello" tls message
### Do you have any idea what may have caused this?
I was able to get this working by putting `curves secp384r1` into the server line directly. So it seems as if the `default-server` options are not copying the `curves`. One might expect to find some `curves` to be copied in https://github.com/haproxy/haproxy/blob/5ce3647dbcfeb9fd552265fbf2499086354578e8/src/server.c#L2758
### Do you have an idea how to solve the issue?
_No response_
### What is your configuration?
This is the whole configuration:
```haproxy
global
ssl-default-server-curves secp384r1
ssl-default-server-options ssl-min-ver TLSv1.3 no-tls-tickets
defaults
mode http
log global
option httplog
option dontlognull
timeout client 1s
timeout connect 1s
timeout server 1s
frontend main
bind *:8080
default_backend app
frontend test
bind *:4040
http-request return status 200 content-type text/plain string OK\n
backend app
default-server ssl verify none no-sni-auto no-check-sni-auto curves secp384r1
server test1 127.0.0.1:4040 # curves secp384r1
```
I am aware that the backend server is HTTP, but the point is the "Client Hello" which doesn't care whether the backend answers or not…
### Output of `haproxy -vv`
```plain
HAProxy version 3.4.5-c28c410 2026/09/24 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2 2031.
Known bugs: http://www.haproxy.org/bugs/bugs-3.4.5.html
Running on: Linux 7.2.7-300.fc45.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Sep 21 18:20:21 UTC 2026 x86_64
Build options :
TARGET = linux-glibc
CC = cc
CFLAGS = -O2 -g -fwrapv -fvect-cost-model=very-cheap
OPTIONS = USE_PTHREAD_EMULATION=1 USE_LINUX_TPROXY=1 USE_GETADDRINFO=1 USE_OPENSSL_AWSLC=1 USE_LUA=1 USE_SLZ=1 USE_TFO=1 USE_QUIC=1 USE_PROMEX=1 USE_PCRE2=1 USE_PCRE2_JIT=1
DEBUG =
Feature list : -51DEGREES +ACCEPT4 +ACME +BACKTRACE -CLOSEFROM +CPU_AFFINITY +CRYPT_H -DEVICEATLAS +DL -ECH -ENGINE +EPOLL -EVPORTS +GETADDRINFO +HAVE_TCP_MD5SIG -KQUEUE +KTLS -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 +TFO +THREAD +THREAD_DUMP +TPROXY +TRACE -WURFL -ZLIB
Detected feature list : +HAVE_WORKING_TCP_MD5SIG
Default settings :
bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
Built with multi-threading support (MAX_TGROUPS=32, MAX_THREADS=1024, default=16).
Built with SSL library version : OpenSSL 1.1.1 (compatible; AWS-LC 4.0.0)
Running on SSL library version : AWS-LC 4.0.0
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library FIPS mode : no
SSL library default verify directory : /etc/ssl/certs
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
QUIC: connection sock-per-conn mode support : yes
QUIC: GSO emission support : yes
Built with Lua version : Lua 5.4.7
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.46 2025-08-27
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with gcc compiler version 14.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)
qmux : mode=HTTP side=FE|BE mux=QMUX flags=HTX|NO_UPG
quic : mode=HTTP side=FE|BE mux=QUIC flags=HTX|NO_UPG|FRAMED
h2 : mode=HTTP side=FE|BE mux=H2 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
fcgi : mode=HTTP side=BE mux=FCGI flags=HTX|HOL_RISK|NO_UPG
<default> : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
spop : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|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] comp-req
[COMP] comp-res
[COMP] compression
[FCGI] fcgi-app
[SPOE] spoe
[TRACE] trace
```
### Last Outputs and Backtraces
```plain
```
### Additional Information
_No response_