# "SSL client certificate not trusted" even with "ca-ignore-err all"

**URL:** <https://discourse.haproxy.org/t/ssl-client-certificate-not-trusted-even-with-ca-ignore-err-all/11972>\
**Category:** Help!\
**Created:** [August 15, 2025, 8:34pm UTC](https://discourse.haproxy.org/t/ssl-client-certificate-not-trusted-even-with-ca-ignore-err-all/11972 "2025-08-15T20:34:14Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![felipeek](https://avatars.discourse-cdn.com/v4/letter/f/bc8723/32.png) [@felipeek](https://discourse.haproxy.org/u/felipeek)\
**Post date:** [August 15, 2025, 8:34pm UTC](https://discourse.haproxy.org/t/ssl-client-certificate-not-trusted-even-with-ca-ignore-err-all/11972/1 "2025-08-15T20:34:14Z")

</div>

Hello!

In my setup, I want haproxy to accept mTLS connections, but not validate the client certificate chain at all. Instead, client’s certificate chain is validated in the backend (necessary because truststores are dynamic).

This is my bind configuration:

```auto
bind :8443 ssl crt-list /etc/haproxy/crt-list.txt ca-file /etc/haproxy/dummy-ca.crt verify optional no-ca-names ca-ignore-err all

```

The ca-file “/etc/haproxy/dummy-ca.crt” is, as the name suggests, a dummy/meaningless CA. I had to provide it otherwise haproxy refuses to offer mTLS (“verify optional”).

I added “ca-ignore-err all” to hint haproxy to ignore the CA validation.

**This setup works only when the received client certificate chain has at least 2 certificates. When the client certificate chain has only 1 certificate, haproxy refuses the connection with:**

Client error: _SSL routines:ST\_OK:tlsv1 alert unknown ca, errno 0_  
Server log: _SSL client certificate not trusted_

It is interesting that, even if I provide a self-signed client certificate (which could be a totally valid trust setup) haproxy simply refuses it. On the other hand, if I provide a chain of 2 certificates, it always accepts it, even if the root CA is not part of the chain.

I believe this behavior is the result of some weird interaction with OpenSSL. Does anyone have any clue? This is unfortunately a major block to me given that clients might authenticate with 1) self-signed certificates or 2) presenting solely the leaf certificate of a chain composed by only 2 certificates, which is also a totally valid scenario assuming the backend trusts the CA, but haproxy would also reject the connection.

Thank you! Appreciate any help!

Haproxy version:

```auto
HAProxy version 3.2.4-98813a1 2025/08/13 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2 2030.
Known bugs: http://www.haproxy.org/bugs/bugs-3.2.4.html
Running on: Linux 6.6.87-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.6.87-0gl0~bp1592 (2025-04-17) x86_64
Build options :
  TARGET = linux-glibc
  CC = cc
  CFLAGS = -O2 -g -fwrapv
  OPTIONS = USE_GETADDRINFO=1 USE_OPENSSL=1 USE_LUA=1 USE_PROMEX=1 USE_PCRE2=1 USE_PCRE2_JIT=1
  DEBUG =

```

---

<div class="post-metadata">

**Author:** ![lukastribus](https://avatars.discourse-cdn.com/v4/letter/l/7ea924/32.png) [@lukastribus](https://discourse.haproxy.org/u/lukastribus)\
**Post date:** [August 18, 2025, 11:10am UTC](https://discourse.haproxy.org/t/ssl-client-certificate-not-trusted-even-with-ca-ignore-err-all/11972/2 "2025-08-18T11:10:47Z")

</div>

Crossposted on github issues, lets proceed in one place only:

> <https://github.com/haproxy/haproxy/issues/3075>
>
> \### Detailed Description of the Problem
> 
> Hello!
> 
> In my setup, I want haproxy to …accept mTLS connections, but not validate the client certificate chain at all. Instead, client’s certificate chain is validated in the backend (necessary because truststores are dynamic).
> 
> This is my bind configuration:
> 
> \`bind :8443 ssl crt-list /etc/haproxy/crt-list.txt ca-file /etc/haproxy/dummy-ca.crt verify optional no-ca-names ca-ignore-err all\`
> 
> The ca-file “/etc/haproxy/dummy-ca.crt” is, as the name suggests, a dummy/meaningless CA. I had to provide it otherwise haproxy refuses to offer mTLS (“verify optional”).
> 
> I added “ca-ignore-err all” to hint haproxy to ignore the CA validation.
> 
> \*\*This setup works only when the received client certificate chain has at least 2 certificates. When the client certificate chain has only 1 certificate, haproxy refuses the connection with:\*\*
> 
> Client error: SSL routines:ST\_OK:tlsv1 alert unknown ca, errno 0
> Server log: SSL client certificate not trusted
> 
> It is interesting that, even if I provide a self-signed client certificate (which could be a totally valid trust setup) haproxy simply refuses it. On the other hand, if I provide a chain of 2 certificates, it always accepts it, even if the root CA is not part of the chain.
> 
> To me, this looks like a bug, or at least an undesired behavior. Please, let me know if you disagree.
> 
> I believe this behavior is the result of some weird interaction with OpenSSL. Does anyone have any clue? This is unfortunately a major block to me given that clients might authenticate with 1) self-signed certificates or 2) presenting solely the leaf certificate of a chain composed by only 2 certificates, which is also a totally valid scenario assuming the backend trusts the CA, but haproxy would also reject the connection.
> 
> Thank you! Appreciate any help!
> 
> \### Expected Behavior
> 
> Expected behavior: haproxy should accept all clients, regardless of whether a valid certification path is found with configured CAs, when \`ca-ignore-err all\` is configured.
> 
> \### Steps to Reproduce the Behavior
> 
> 1. Bind via e.g. \`bind :8443 ssl crt-list /etc/haproxy/crt-list.txt ca-file /etc/haproxy/dummy-ca.crt verify optional no-ca-names ca-ignore-err all\`. 
> 2. Make a request using a self-signed client certificate. It will fail in the handshake with SSL routines:ST\_OK:tlsv1 alert unknown ca, errno 0 (or similar, depending on client's TLS engine).
> 
> \### Do you have any idea what may have caused this?
> 
> \_No response\_
> 
> \### Do you have an idea how to solve the issue?
> 
> \_No response\_
> 
> \### What is your configuration?
> 
> \`\`\`haproxy
> My configuration is a bit complex, but most of it does not matter for this problem. I think the relevant parts are the \`bind\` statement and the \`crt-list\`
> 
> haproxy.cfg:
> 
> global
> log stdout format raw local0
> stats socket ipv4@127.0.0.1:9999 level admin
> lua-load /etc/haproxy/lua/vip-pick.lua
>     
> defaults
> log global
> timeout connect 3s
> timeout client 30s
> timeout server 30s
> # helpful end log to see tenant chosen by SPOE or fallback
> log-format "END fe=%f be=%b srv=%\[srv\_name\] sni=%\[ssl\_fc\_sni\] tenant=%\[var(txn.tenant.tenant)\] tls\_fc=%\[ssl\_fc\] tls\_bc=%\[ssl\_bc\] Tt=%Tt ssl\_c\_used=%\[ssl\_c\_used\] ssl\_c\_verify=%\[ssl\_c\_verify\] ssl\_c\_err=%\[ssl\_c\_err\] ssl\_c\_ca\_err=%\[ssl\_c\_ca\_err\] ssl\_fc\_protocol=%\[ssl\_fc\_protocol\]"
>     
> # SPOE agent health
> backend bk\_spoa
> mode tcp
> option spop-check
> server spoa spoa-tenant.ns.svc.cluster.local:9000 check
> 
> frontend fe\_tcp\_443
> mode tcp
> bind :8443 ssl crt-list /etc/haproxy/crt-list.txt ca-file /etc/haproxy/dummy-ca.crt verify optional no-ca-names ca-ignore-err all
> tcp-request inspect-delay 10m
>     
> acl is\_h2 ssl\_fc\_alpn -i h2
> acl is\_h11 ssl\_fc\_alpn -i http/1.1
> acl is\_http\_alpn ssl\_fc\_alpn -i h2 http/1.1
> acl is\_h1 payload(0,8) -m reg -i "^(GET|POST|HEAD|PUT|OPTIONS|DELETE|PATCH|TRACE|CONNECT)\[\\t \]"
>     
> # TCP-path work first (executed before switching)
> filter spoe engine tenant\_spoa config /etc/haproxy/spoe/tenant.spocfg
>     
> # Run TCP SPOE only if it's NOT HTTP (no ALPN h2/h1.1 and not H1-bytes)
> tcp-request content send-spoe-group tenant\_spoa grp\_tcp if { ssl\_fc } !is\_http\_alpn !is\_h1
>     
> # Fallback tenant from SNI (only on the TCP path)
> tcp-request content set-var(txn.tenant.tenant) ssl\_fc\_sni if { ssl\_fc } !{ var(txn.tenant.tenant) -m found }
>     
> # End inspection immediately for the TCP path (don’t end it for HTTP)
> tcp-request content accept if { ssl\_fc } !is\_http\_alpn !is\_h1
>     
> # Switch after all tcp-request rules have been evaluated
> use\_backend bk\_http\_app if is\_http\_alpn
> use\_backend bk\_http\_app if is\_h1
> default\_backend bk\_tcp\_app
> 
> # Plain TCP backend that only forwards, with PPv2 + custom TLV = Tenant
> backend bk\_tcp\_app
> mode tcp
> 
> # Default TLS client settings for all servers in this backend
> default-server ssl verify required ca-file /etc/haproxy/app-server/tls.crt crt /etc/haproxy/certs/haproxy-client.pem sni ssl\_fc\_sni
> 
> hash-type consistent
> balance hash var(txn.tenant.tenant)
> 
> tcp-request content set-var(txn.tenant.tenant) ssl\_fc\_sni if !{ var(txn.tenant.tenant) -m found }
>       
> # subkey = "tenant:requestId"
> tcp-request content set-var-fmt(txn.subkey) "%\[var(txn.tenant.tenant)\]:%\[var(txn.reqid)\]"
> 
> tcp-request content set-var(txn.reqid) var(txn.tenant.requestId)
> tcp-request content set-var(txn.reqid) ssl\_fc\_sni if !{ var(txn.reqid) -m found }
> tcp-request content set-var-fmt(txn.subkey) "%\[var(txn.tenant.tenant)\]:%\[var(txn.reqid)\]"
>       
> tcp-request content set-var(txn.allowed\_csv) var(txn.tenant.tenant),map(/etc/haproxy/tenant-shards.map)
> tcp-request content set-var(txn.vip\_server) lua.pick\_server()
> use-server %\[var(txn.vip\_server)\] if { var(txn.allowed\_csv) -m found } { var(txn.vip\_server) -m found }
>   
> # Send PPv2 to the app, injecting custom Tenant TLV (0xE1)
> server s0 app-shard-0.ns.svc.cluster.local:9090 sni str(app-shard-0.ns.svc.cluster.local) send-proxy-v2 set-proxy-v2-tlv-fmt(0xE1) "%\[var(txn.tenant.tenant)\]" track bk\_http\_app/s0
> server s1 app-shard-1.ns.svc.cluster.local:9090 sni str(app-shard-1.ns.svc.cluster.local) send-proxy-v2 set-proxy-v2-tlv-fmt(0xE1) "%\[var(txn.tenant.tenant)\]" track bk\_http\_app/s1
> server s2 app-shard-2.ns.svc.cluster.local:9090 sni str(app-shard-2.ns.svc.cluster.local) send-proxy-v2 set-proxy-v2-tlv-fmt(0xE1) "%\[var(txn.tenant.tenant)\]" track bk\_http\_app/s2
> server s3 app-shard-3.ns.svc.cluster.local:9090 sni str(app-shard-3.ns.svc.cluster.local) send-proxy-v2 set-proxy-v2-tlv-fmt(0xE1) "%\[var(txn.tenant.tenant)\]" track bk\_http\_app/s3
> 
> backend bk\_http\_app
> mode http
> 
> # SPOE for HTTP flows happens in the HTTP backend
> filter spoe engine tenant\_spoa config /etc/haproxy/spoe/tenant.spocfg
> http-request send-spoe-group tenant\_spoa grp\_http
>     
> # Fallback if agent didn’t set a tenant (use first label from SNI)
> http-request set-var(txn.tenant.tenant) ssl\_fc\_sni if !{ var(txn.tenant.tenant) -m found }
> 
> # Default TLS client settings for all servers in this backend
> default-server ssl verify required ca-file /etc/haproxy/app-server/tls.crt crt /etc/haproxy/certs/haproxy-client.pem sni ssl\_fc\_sni check-ssl
> 
> # Default: consistent hash across ALL shards (non-VIPs)
> hash-type consistent
> balance hash var(txn.tenant.tenant)
>     
> # --- VIP override ---
>     
> # Build reqid with fallbacks: SPOA → X-Request-Id → path
> http-request set-var(txn.reqid) var(txn.tenant.requestId)
> http-request set-var(txn.reqid) req.hdr(X-Request-Id) if { var(txn.reqid) -m len 0 }
> http-request set-var(txn.reqid) path if { var(txn.reqid) -m len 0 }
>     
> # Build subkey = "tenant:requestId"
> http-request set-var-fmt(txn.subkey) "%\[var(txn.tenant.tenant)\]:%\[var(txn.reqid)\]"
> 
> # VIP lookup by tenant only
> http-request set-var(txn.allowed\_csv) var(txn.tenant.tenant),map(/etc/haproxy/tenant-shards.map)
>       
> # pick server & override (Lua fetch reads txn.allowed\_csv + txn.subkey)
> http-request set-var(txn.vip\_server) lua.pick\_server()
> 
> # --- ---
> 
> # ---- Debug request headers to BACKEND ----
> http-request set-header X-HA-tenant "%\[var(txn.tenant.tenant)\]"
> http-request set-header X-HA-allowed "%\[var(txn.allowed\_csv)\]"
> http-request set-header X-HA-subkey "%\[var(txn.subkey)\]"
> http-request set-header X-HA-vipsrv "%\[var(txn.vip\_server)\]"
> 
> # Override only if BOTH a VIP entry exists and Lua returned a server
> use-server %\[var(txn.vip\_server)\] if { var(txn.allowed\_csv) -m found } { var(txn.vip\_server) -m found }
>     
> # ---- Debug response headers back to CLIENT ----
> http-response add-header X-HA-tenant "%\[var(txn.tenant.tenant)\]"
> http-response add-header X-HA-server "%\[srv\_name\]"
> http-response add-header X-HA-allowed "%\[var(txn.allowed\_csv)\]"
> http-response add-header X-HA-subkey "%\[var(txn.subkey)\]"
> http-response add-header X-HA-vipsrv "%\[var(txn.vip\_server)\]"
>     
> # Shard servers (names must match "s\<idx\>" used by Lua)
> server s0 app-shard-0.ns.svc.cluster.local:8080 sni str(app-shard-0.ns.svc.cluster.local) check
> server s1 app-shard-1.ns.svc.cluster.local:8080 sni str(app-shard-1.ns.svc.cluster.local) check
> server s2 app-shard-2.ns.svc.cluster.local:8080 sni str(app-shard-2.ns.svc.cluster.local) check
> server s3 app-shard-3.ns.svc.cluster.local:8080 sni str(app-shard-3.ns.svc.cluster.local) check
> 
> tenant.spocfg:
> 
> \[tenant\_spoa\]
> spoe-agent tenant\_spoa
> groups grp\_tcp grp\_http
> option var-prefix tenant
> option continue-on-error
> timeout hello 2000ms
> timeout idle 30s
> timeout processing 1000ms
> use-backend bk\_spoa
> max-frame-size 16380
>     
> spoe-message msg\_tcp
> args sni=ssl\_fc\_sni c\_chain\_b64=ssl\_c\_chain\_der,base64 c\_leaf\_b64=ssl\_c\_der,base64
>     
> spoe-group grp\_tcp
> messages msg\_tcp
>     
> spoe-message msg\_http
> args tenant\_hdr=req.hdr(tenant) sni=ssl\_fc\_sni host=req.hdr(Host) c\_chain\_b64=ssl\_c\_chain\_der,base64 c\_leaf\_b64=ssl\_c\_der,base64
>     
> spoe-group grp\_http
> messages msg\_http
> 
> crt-list.txt:
> 
> /etc/haproxy/certs/example.pem \[alpn h2,http/1.1\] \*.example.com example.com no-ca-names ca-ignore-err all
> /etc/haproxy/certs/internal.pem \[alpn h2,http/1.1\] \*.internal.com internal.com no-ca-names ca-ignore-err all
> \`\`\`
> 
> \### Output of \`haproxy -vv\`
> 
> \`\`\`plain
> HAProxy version 3.2.4-98813a1 2025/08/13 - https://haproxy.org/
> Status: long-term supported branch - will stop receiving fixes around Q2 2030.
> Known bugs: http://www.haproxy.org/bugs/bugs-3.2.4.html
> Running on: Linux 6.6.87-cloud-amd64 #1 SMP PREEMPT\_DYNAMIC Debian 6.6.87-0gl0~bp1592 (2025-04-17) x86\_64
> Build options :
> TARGET = linux-glibc
> CC = cc
> CFLAGS = -O2 -g -fwrapv
> OPTIONS = USE\_GETADDRINFO=1 USE\_OPENSSL=1 USE\_LUA=1 USE\_PROMEX=1 USE\_PCRE2=1 USE\_PCRE2\_JIT=1
> DEBUG =
> 
> 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 +SLZ +SSL -STATIC\_PCRE -STATIC\_PCRE2 +TFO +THREAD +THREAD\_DUMP +TPROXY -WURFL -ZLIB +ACME
> 
> Default settings :
> bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
> 
> Built with multi-threading support (MAX\_TGROUPS=32, MAX\_THREADS=1024, default=8).
> Built with SSL library version : OpenSSL 3.5.1 1 Jul 2025
> Running on SSL library version : OpenSSL 3.5.1 1 Jul 2025
> SSL library supports TLS extensions : yes
> SSL library supports SNI : yes
> SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
> OpenSSL providers loaded : default
> 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.45 2025-02-05
> 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)
> 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\] compression
> \[FCGI\] fcgi-app
> \[SPOE\] spoe
> \[TRACE\] trace
> \`\`\`
> 
> \### Last Outputs and Backtraces
> 
> \`\`\`plain
> Client side:
> 
> \* Request completely sent off
> \* LibreSSL SSL\_read: LibreSSL/3.3.6: error:1404C418:SSL routines:ST\_OK:tlsv1 alert unknown ca, errno 0
> \* Failed receiving HTTP2 data: 56(Failure when receiving data from the peer)
> \* Connection #0 to host \<redac\> left intact
> curl: (56) LibreSSL SSL\_read: LibreSSL/3.3.6: error:1404C418:SSL routines:ST\_OK:tlsv1 alert unknown ca, errno 0
> 
> Server side (log):
> 100.96.4.1:50302 \[15/Aug/2025:20:07:14.101\] fe\_tcp\_443/1: SSL client certificate not trusted
> \`\`\`
> 
> \### Additional Information
> 
> \_No response\_
