Your Feature Request
Hi,
currently backend cert validation happens via verifyhost in a server directive. It's capabilities to verify backend certs are limited to checking subject or DNS SANs via server [...] verifyhost example.com [...].
The workload attestation framework SPIFFE/SPIRE works with URI SANs in X.509 certs. These URIs are the intended validation target and not the subject name (example spiffe://example.org/ns/default/sa/default/frontend). HAProxy currently does not support this, which makes it necessary to configure legacy DNS SAN support, which is less expressive and awkward to use. In my perception especially in Kubernetes environments SPIFFE/SPIRE has become quite popular.
The feature request could end here, but if the underlying code is touched anyways we can add more convenience: X.509 also supports IP and email SANs. Supporting them for more flexibility is not much added work. Especially IP SANs are useful in environments with static IP addresses and no DNS infra or operators not wanting to rely on DNS.
Finally it can be useful to validate against a list of SANs where any match makesvalidation pass. This is useful in migration scenarios where SPIFFE authorities or IPs are migrated and you want to support multiple correct values for a time.
I already designed a possible solution. In short this is the current design:
Backend cert:
- Subject CN:
example.com
- SANs:
DNS:example.com, DNS:foo.tld, URI:spiffe://bar
Assuming the client request carries Host: example.com (so auto-SNI yields example.com); add no-sni-auto where indicated.
sni |
verifyhost |
SNI sent |
Verified against |
Outcome |
Remarks |
| — |
— |
example.com (auto) |
SNI vs DNS SAN + CN |
OK |
matches DNS:example.com |
| — |
— (+no-sni-auto) |
none |
nothing |
OK |
chain valid; no name pinning |
| — |
example.com |
example.com (auto) |
SNI vs DNS SAN + CN |
OK |
SNI overrides verifyhost; matches DNS:example.com |
| — |
example.com (+no-sni-auto) |
none |
verifyhost vs DNS SAN + CN |
OK |
matches DNS:example.com |
| — |
mismatch.example (+no-sni-auto) |
none |
verifyhost vs DNS SAN + CN |
FAIL |
no DNS SAN or CN matches mismatch.example |
str(foo.tld) |
example.com |
foo.tld |
SNI vs DNS SAN + CN |
OK |
SNI overrides verifyhost; matches DNS:foo.tld |
str(other.tld) |
example.com |
other.tld |
SNI vs DNS SAN + CN |
FAIL |
classic gotcha — SNI overrides verifyhost, doesn't match cert |
The important thing to note about the status quo is that users have to be aware that setting sni overrides certificate validation. It is not possible to validate foo.tld while sending bar.tld as SNI. Probably because the possible use-cases for that are very few.
When introducing validation against a list of SANs that may not even be DNS SANs then this requirement becomes vital. I suggest extending the verifyhost parameter as follows:
sni |
verifyhost |
SNI sent |
Verified against |
Outcome |
Remarks |
| — |
example.com,foo.tld |
example.com (auto) |
list vs DNS SAN + CN |
OK |
multi-value: SNI is sent but NOT overriding; list matches DNS:example.com |
str(other.tld) |
example.com,foo.tld |
other.tld |
list vs DNS SAN + CN |
OK |
SNI sent for routing; list matches DNS:example.com; SNI itself irrelevant to verification |
| — |
dns:example.com |
example.com (auto) |
list vs DNS SAN only |
OK |
typed entry: SNI sent but NOT overriding; no CN fallback |
str(other.tld) |
dns:example.com |
other.tld |
list vs DNS SAN only |
OK |
typed entry bypasses SNI override; matches DNS:example.com |
str(example.com) |
uri:spiffe://bar |
example.com |
list vs URI SAN |
OK |
new capability: SNI selects backend cert, URI pins workload identity |
str(example.com) |
uri:spiffe://wrong |
example.com |
list vs URI SAN |
FAIL |
URI SAN is spiffe://bar, not wrong; SNI match is irrelevant |
| — |
dns:nope,uri:spiffe://bar (+no-sni-auto) |
none |
list vs DNS SAN + URI SAN |
OK |
OR semantics: dns:nope misses, uri:spiffe://bar matches |
verifyhost can now have multiple comma-separated entries and on first match validation is successful (OR semantics). Entries can be prefixed via dns: , uri:, ip: or email: to be typed. Entries without such a prefix are validated against DNS SANs and CN as fallback. dns: prefixed entries are validated against DNS SANs only.
Important: The design does not change the behavior of the first table and is fully backward compatible. But as soon as verifyhost is not a single unprefixed entry sni can not override cert validation anymore.
I am aware that extending verifyhost may be controversial as it is a bit of a misnomer. We verify more than hostnames at this point. But I do think introducing another parameter like verify-sans may cause even more confusion as there are now two parameters influencing certificate validation via SANs. This is why I think reusing verifyhost is the lesser of two evils even with the sni override exception.
I am not a big C dev, but I wanted to test if implementation was feasible so I used an LLM to generate an example implementation including vtc tests. It is probably not good enough to be merged but it is at least a start.
What are you trying to do?
I would like to properly verify backend server certificates issued by SPIFFE/SPIRE.
Output of haproxy -vv
HAProxy version 3.4-dev11-672986-64 2026/05/13 - https://haproxy.org/
Status: development branch - not safe for use in production.
Known bugs: https://github.com/haproxy/haproxy/issues?q=is:issue+is:open
Running on: Linux 7.0.3-1-cachyos #1 SMP PREEMPT Fri, 01 May 2026 19:46:26 +0000 x86_64
Build options :
TARGET = linux-glibc
CC = cc
CFLAGS = -O2 -g -fwrapv -fvect-cost-model=very-cheap
OPTIONS = USE_OPENSSL=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 -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 3.6.2 7 Apr 2026
Running on SSL library version : OpenSSL 3.6.2 7 Apr 2026
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library default verify directory : /etc/ssl/certs
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
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 without PCRE or PCRE2 support (using libc's regex instead)
Encrypted password support via crypt(3): yes
Built with gcc compiler version 16.1.1 20260430
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 : none
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
Edit: Forgot to mention that apparently a very similar feature has been suggested in 2020, but stalled on design questions.
Your Feature Request
Hi,
currently backend cert validation happens via
verifyhostin aserverdirective. It's capabilities to verify backend certs are limited to checking subject or DNS SANs viaserver [...] verifyhost example.com [...].The workload attestation framework SPIFFE/SPIRE works with URI SANs in X.509 certs. These URIs are the intended validation target and not the subject name (example
spiffe://example.org/ns/default/sa/default/frontend). HAProxy currently does not support this, which makes it necessary to configure legacy DNS SAN support, which is less expressive and awkward to use. In my perception especially in Kubernetes environments SPIFFE/SPIRE has become quite popular.The feature request could end here, but if the underlying code is touched anyways we can add more convenience: X.509 also supports IP and email SANs. Supporting them for more flexibility is not much added work. Especially IP SANs are useful in environments with static IP addresses and no DNS infra or operators not wanting to rely on DNS.
Finally it can be useful to validate against a list of SANs where any match makesvalidation pass. This is useful in migration scenarios where SPIFFE authorities or IPs are migrated and you want to support multiple correct values for a time.
I already designed a possible solution. In short this is the current design:
Backend cert:
example.comDNS:example.com,DNS:foo.tld,URI:spiffe://barAssuming the client request carries
Host: example.com(so auto-SNI yieldsexample.com); addno-sni-autowhere indicated.sniverifyhostexample.com(auto)DNS:example.comno-sni-auto)example.comexample.com(auto)verifyhost; matchesDNS:example.comexample.com(+no-sni-auto)verifyhostvs DNS SAN + CNDNS:example.commismatch.example(+no-sni-auto)verifyhostvs DNS SAN + CNmismatch.examplestr(foo.tld)example.comfoo.tldverifyhost; matchesDNS:foo.tldstr(other.tld)example.comother.tldverifyhost, doesn't match certThe important thing to note about the status quo is that users have to be aware that setting
snioverrides certificate validation. It is not possible to validatefoo.tldwhile sendingbar.tldas SNI. Probably because the possible use-cases for that are very few.When introducing validation against a list of SANs that may not even be DNS SANs then this requirement becomes vital. I suggest extending the
verifyhostparameter as follows:sniverifyhostexample.com,foo.tldexample.com(auto)DNS:example.comstr(other.tld)example.com,foo.tldother.tldDNS:example.com; SNI itself irrelevant to verificationdns:example.comexample.com(auto)str(other.tld)dns:example.comother.tldDNS:example.comstr(example.com)uri:spiffe://barexample.comstr(example.com)uri:spiffe://wrongexample.comspiffe://bar, notwrong; SNI match is irrelevantdns:nope,uri:spiffe://bar(+no-sni-auto)dns:nopemisses,uri:spiffe://barmatchesverifyhostcan now have multiple comma-separated entries and on first match validation is successful (OR semantics). Entries can be prefixed viadns:,uri:,ip:oremail:to be typed. Entries without such a prefix are validated against DNS SANs and CN as fallback.dns:prefixed entries are validated against DNS SANs only.Important: The design does not change the behavior of the first table and is fully backward compatible. But as soon as
verifyhostis not a single unprefixed entrysnican not override cert validation anymore.I am aware that extending
verifyhostmay be controversial as it is a bit of a misnomer. We verify more than hostnames at this point. But I do think introducing another parameter likeverify-sansmay cause even more confusion as there are now two parameters influencing certificate validation via SANs. This is why I think reusingverifyhostis the lesser of two evils even with thesnioverride exception.I am not a big C dev, but I wanted to test if implementation was feasible so I used an LLM to generate an example implementation including vtc tests. It is probably not good enough to be merged but it is at least a start.
What are you trying to do?
I would like to properly verify backend server certificates issued by SPIFFE/SPIRE.
Output of
haproxy -vvEdit: Forgot to mention that apparently a very similar feature has been suggested in 2020, but stalled on design questions.