Summary
kafka-python 3.0.x only sends the TLS SNI extension when ssl_check_hostname is true. kafka-python ≤ 2.x always sent it. Any deployment where TLS connections are routed by SNI — Kubernetes TLS-passthrough ingresses (Strimzi type: ingress listeners, Istio Gateway TLSRoute, nginx ssl_preread, HAProxy req_ssl_sni) — becomes unreachable for clients that disable hostname verification, which is common with private/self-signed cluster CAs.
The failure is hard to diagnose from the client side: the router can't match a route and drops the connection mid-handshake, so bootstrap fails with
KafkaConnectionError: Handshake failed: [Errno 104] Connection reset by peer
kafka.errors.KafkaTimeoutError: Unable to bootstrap from [...]
with nothing suggesting SNI as the cause.
I searched existing issues for SNI, server_hostname, ssl_check_hostname, and post-3.0 handshake/connection-reset reports and found no prior report of this — apologies if I missed one. Closest prior art is #1957, which asked for clarification of ssl_check_hostname semantics back in the 2.x era; the answer there (and 2.x behavior) treated the flag as governing hostname verification only. In 3.0 it now additionally governs whether SNI is sent at all, which is a silent wire-behavior change relative to every 2.x release.
Root cause
kafka/net/transport.py (master @ 7c5003a5, present since 3.0.0):
class KafkaSSLTransport(KafkaTCPTransport):
def __init__(self, net, sock, ssl_context, host=None, ssl_check_hostname=False):
self._ssl_context = ssl_context
server_hostname = host if ssl_check_hostname else None # <-- line 376
sock = ssl_context.wrap_socket(
sock, server_hostname=server_hostname, do_handshake_on_connect=False)
2.x behavior for comparison (kafka/conn.py):
self._sock = self._ssl_context.wrap_socket(
self._sock,
server_hostname=self.host.rstrip("."),
do_handshake_on_connect=False)
This couples two independent concerns: server_hostname controls what SNI is sent and what name is verified, but whether verification happens at all is governed by SSLContext.check_hostname — which the client already sets from ssl_check_hostname in _build_ssl_context(). Passing server_hostname=None to suppress verification also suppresses SNI.
Reproduction
Minimal setup: an SSL broker behind an nginx ssl_preread passthrough router that routes by SNI (config below), client configured with ssl_check_hostname=False.
KafkaProducer(
bootstrap_servers=["kafka-ingress:9096"],
security_protocol="SSL",
ssl_cafile="cluster-ca.pem",
ssl_certfile="client.crt",
ssl_keyfile="client.key",
ssl_check_hostname=False, # 2.x: works; 3.0.x: bootstrap times out
)
# nginx.conf (stream) — stand-in for any SNI-routing ingress
stream {
map $ssl_preread_server_name $backend {
kafka-ingress kafka:9095;
default 127.0.0.1:9; # unroutable: no/unknown SNI
}
server { listen 9096; ssl_preread on; proxy_pass $backend; }
}
Observed (3.0.7, also reproduced on 3.0.2): with ssl_check_hostname=True the ClientHello carries SNI, nginx routes it, bootstrap succeeds. With ssl_check_hostname=False no SNI is sent, nginx can't route, and every bootstrap attempt dies mid-handshake until bootstrap_timeout_ms expires. Identical config on kafka-python 2.x works in both cases. nginx access logs confirm: sni="kafka-ingress" vs sni="". I can attach a self-contained docker-compose reproduction if useful.
Real-world instance: producers connecting to a Strimzi-managed cluster on EKS through an Istio Gateway in TLS-passthrough mode (SNI-matched TLSRoutes) crash at construction after upgrading 2.x → 3.0.x, with ssl_check_hostname=False set because the cluster uses a private CA.
Proposed fix
Always pass server_hostname; let the context decide whether to verify it (wrap_socket ignores the name for verification purposes when context.check_hostname is false):
server_hostname = host.rstrip(".") if host else None
This restores 2.x wire behavior (including the trailing-dot strip for FQDNs) and resolves the #1957-style ambiguity in the conservative direction: ssl_check_hostname goes back to meaning verification only. As a bonus it fixes a latent ValueError: check_hostname requires server_hostname for users who pass a custom ssl_context with check_hostname=True while leaving ssl_check_hostname=False in the client config.
Happy to submit a PR.
Summary
kafka-python 3.0.x only sends the TLS SNI extension when
ssl_check_hostnameis true. kafka-python ≤ 2.x always sent it. Any deployment where TLS connections are routed by SNI — Kubernetes TLS-passthrough ingresses (Strimzitype: ingresslisteners, Istio GatewayTLSRoute, nginxssl_preread, HAProxyreq_ssl_sni) — becomes unreachable for clients that disable hostname verification, which is common with private/self-signed cluster CAs.The failure is hard to diagnose from the client side: the router can't match a route and drops the connection mid-handshake, so bootstrap fails with
with nothing suggesting SNI as the cause.
I searched existing issues for SNI,
server_hostname,ssl_check_hostname, and post-3.0 handshake/connection-reset reports and found no prior report of this — apologies if I missed one. Closest prior art is #1957, which asked for clarification ofssl_check_hostnamesemantics back in the 2.x era; the answer there (and 2.x behavior) treated the flag as governing hostname verification only. In 3.0 it now additionally governs whether SNI is sent at all, which is a silent wire-behavior change relative to every 2.x release.Root cause
kafka/net/transport.py(master @7c5003a5, present since 3.0.0):2.x behavior for comparison (
kafka/conn.py):This couples two independent concerns:
server_hostnamecontrols what SNI is sent and what name is verified, but whether verification happens at all is governed bySSLContext.check_hostname— which the client already sets fromssl_check_hostnamein_build_ssl_context(). Passingserver_hostname=Noneto suppress verification also suppresses SNI.Reproduction
Minimal setup: an SSL broker behind an nginx
ssl_prereadpassthrough router that routes by SNI (config below), client configured withssl_check_hostname=False.Observed (3.0.7, also reproduced on 3.0.2): with
ssl_check_hostname=Truethe ClientHello carries SNI, nginx routes it, bootstrap succeeds. Withssl_check_hostname=Falseno SNI is sent, nginx can't route, and every bootstrap attempt dies mid-handshake untilbootstrap_timeout_msexpires. Identical config on kafka-python 2.x works in both cases. nginx access logs confirm:sni="kafka-ingress"vssni="". I can attach a self-contained docker-compose reproduction if useful.Real-world instance: producers connecting to a Strimzi-managed cluster on EKS through an Istio Gateway in TLS-passthrough mode (SNI-matched
TLSRoutes) crash at construction after upgrading 2.x → 3.0.x, withssl_check_hostname=Falseset because the cluster uses a private CA.Proposed fix
Always pass
server_hostname; let the context decide whether to verify it (wrap_socketignores the name for verification purposes whencontext.check_hostnameis false):This restores 2.x wire behavior (including the trailing-dot strip for FQDNs) and resolves the #1957-style ambiguity in the conservative direction:
ssl_check_hostnamegoes back to meaning verification only. As a bonus it fixes a latentValueError: check_hostname requires server_hostnamefor users who pass a customssl_contextwithcheck_hostname=Truewhile leavingssl_check_hostname=Falsein the client config.Happy to submit a PR.