Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support for validating backend certificates with multiple DNS, URI, IP and email SANs #3373

@phihos

Description

@phihos

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: featureThis issue describes a feature request / wishlist.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions