ZooKeeper TLS handshake failures: unsuccessful handshakes and non-mTLS connections
When ZooKeeper is configured for TLS, the TLS layer becomes a new failure surface between clients and the ensemble. A spike in zk_unsuccessful_handshake or zk_tls_handshake_exceeded means clients are attempting TLS connections that never complete. If your environment requires mutual TLS, any non-zero value in zk_non_mtls_remote_conn_count means a client bypassed mTLS entirely.
These failures are noisy in a particular way. The client sees a connection timeout or a refused handshake, while the server logs an “Unsuccessful handshake” entry that does not always explain why. Certificate expiry, cipher mismatch, hostname verification failure, and low host entropy all produce similar surface symptoms. The fix is rarely the server itself; it is usually a client configuration error, an expired credential, or a monitoring tool sending plaintext to a secure port.
What this means
TLS in ZooKeeper is layered on top of the Netty connection factory. When a client connects to the secure client port, ZooKeeper must complete a TLS handshake before it can process any session establishment. If the handshake fails, whether from a bad certificate, a cipher mismatch, or plaintext bytes sent to a TLS port, the connection is dropped before a session is ever created.
The signals to track:
zk_unsuccessful_handshake: counter of TLS handshakes that failed to complete. Increments whenever a client connects to the secure port but cannot negotiate TLS successfully.zk_tls_handshake_exceeded: counter of TLS handshakes that exceeded the configured handshake timeout. This is the slow-handshake signal. The client started a handshake but the server could not finish it in time. Low host entropy is a classic cause.zk_non_mtls_remote_conn_count: count of remote connections that completed without mutual TLS. In an mTLS-required environment this should be zero. A non-zero count means a client connected without presenting a client certificate, which indicates either a misconfigured port or a policy bypass.
A critical caveat: certificate expiry is a silent, total outage once every client uses TLS. ZooKeeper does not expose certificate expiry as a metric. You must check it externally with openssl or an equivalent tool. An expired server certificate causes every TLS client to fail the handshake, but zk_unsuccessful_handshake will not tell you “the cert expired.” It will just count the failures.
flowchart TD
A["Handshake failures
on secure port"] --> B{"Cert valid?"}
B -->|No| C["Renew and reload cert"]
B -->|Yes| D{"Plaintext bytes
to TLS port?"}
D -->|Yes| E["Fix client port
or use TLS client"]
D -->|No| F{"Handshakes timing out?"}
F -->|Yes| G["Check host entropy"]
F -->|No| H["Cipher or SAN mismatch"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Expired server or client certificate | Every TLS client fails handshake simultaneously; zk_unsuccessful_handshake spikes | openssl s_client -connect host:secureClientPort and inspect notAfter |
| Plaintext client hitting secure port | NotSslRecordException in server log; monitoring tools (ruok/mntr via nc) fail | Client connection string port number |
| Cipher or protocol mismatch | Handshake fails on specific JDK versions or distributions; works elsewhere | TLS protocol version and cipher list on both ends |
| Hostname verification failure | Client connects, cert is valid, but SAN does not match hostname the client used | Certificate SAN entries versus client connect string |
| Low host entropy | zk_tls_handshake_exceeded increments; handshakes stall then complete or time out | /proc/sys/kernel/random/entropy_avail |
| Wrong client TLS property | Client logs “Unsuccessful handshake with session 0x0” despite correct cert | Client JVM property: zookeeper.client.secure not zookeeper.ssl.client.enable |
Quick checks
These are read-only and safe to run during an incident. Adjust the secure client port and log path to match your deployment. If only the secure port is enabled and the plaintext port (2181) is closed, use a TLS-capable client for mntr instead of nc.
# Check TLS handshake counters via mntr
echo mntr | nc localhost 2181 | grep -E 'zk_unsuccessful_handshake|zk_tls_handshake_exceeded|zk_non_mtls'
# Verify the server certificate is not expired
echo | openssl s_client -connect localhost:2281 -showcerts 2>/dev/null | openssl x509 -noout -enddate -subject -issuer
# Check available host entropy (low values can stall TLS handshakes)
cat /proc/sys/kernel/random/entropy_avail
# Confirm which port is the secure client port versus plaintext
grep -E 'secureClientPort|clientPort|portUnification' /etc/zookeeper/zoo.cfg
# Check whether mTLS is required
grep 'ssl.clientAuth' /etc/zookeeper/zoo.cfg
# Look for TLS-specific errors in the server log
grep -E 'Unsuccessful handshake|NotSslRecordException|SSLHandshakeException' /var/log/zookeeper/zookeeper.log | tail -30
# Test a TLS connection from the server host to itself
# Add -cert/-key flags if mTLS is required
echo ruok | openssl s_client -connect localhost:2281 -quiet 2>/dev/null
How to diagnose it
Confirm the scope. Is
zk_unsuccessful_handshakeincrementing on all ensemble members or just one? A single-node pattern points to a node-specific cert or config problem. All nodes points to a fleet-wide cert expiry or a widespread client misconfiguration.Check certificate validity first. Cert expiry is the highest-priority cause because it is a silent total outage. Run
openssl s_clientagainst the secure port on each node and inspectnotAfter. If the cert expired, no amount of server tuning will fix the handshakes until the cert is renewed and reloaded.Inspect the server log for the specific failure. “Unsuccessful handshake” is the generic signal. The log line near it usually contains the real cause:
NotSslRecordException(plaintext to TLS port),SSLHandshakeException(cert or cipher problem), or a timeout (entropy or network). Filter the log by the failing client IP if known.Distinguish client misconfiguration from a server problem. If a single client or client type fails while others succeed, the server is fine. The failing client is either using the wrong port, the wrong TLS property, an expired client cert, or a JDK with incompatible cipher defaults.
Verify mTLS enforcement. If
zk_non_mtls_remote_conn_countis non-zero in an mTLS-required environment, identify which clients connected without a client certificate. Check whether port unification is enabled (which accepts both TLS and plaintext on one port) and whetherssl.clientAuthis set toneed.Check host entropy. If
zk_tls_handshake_exceededis incrementing but handshakes eventually succeed, the host may be starved for entropy. This is more common on virtualized hosts and containers without a hardware RNG.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
zk_unsuccessful_handshake | Counts failed TLS negotiations on the secure port | Any sustained non-zero rate |
zk_tls_handshake_exceeded | Counts handshakes that timed out during negotiation | Non-zero rate indicates entropy, CPU, or network pressure |
zk_non_mtls_remote_conn_count | Remote connections that skipped mTLS | Non-zero in mTLS-required environments |
zk_non_mtls_local_conn_count | Local connections without mTLS | Often acceptable; investigate if policy requires mTLS for localhost |
Certificate notAfter | ZooKeeper does not expose cert expiry as a metric | Any cert within 30 days of expiry |
/proc/sys/kernel/random/entropy_avail | Low entropy stalls TLS handshakes | Sustained values below 200 |
Fixes
Expired certificate
Renew the certificate in your keystore and restart the affected ZooKeeper node. If the cert is managed by an external CA or cert-manager, trigger the renewal and verify the keystore was updated before restarting. Older ZooKeeper versions do not hot-reload TLS certificates, so a rolling restart is required.
For client certificates the same applies. The client must renew and reload its keystore. An expired client cert produces the same “Unsuccessful handshake” on the server side.
Plaintext client hitting the secure port
The most common offender is a monitoring tool using nc or a raw TCP socket to send ruok or mntr to what is now the secure port. The fix is to point the monitoring tool at the plaintext client port (default 2181) if it is still open, or to use a TLS-capable client for the health check.
If port unification is enabled, both TLS and plaintext are accepted on one port. Verify the client connects to the unified port and is not sending plaintext to a TLS-only port or TLS to a plaintext-only port.
Wrong client TLS property
The correct client-side JVM property to enable TLS is zookeeper.client.secure=true. The property zookeeper.ssl.client.enable=true does not work and produces “Unsuccessful handshake with session 0x0” on the server. This is the most frequent client-side misconfiguration.
Cipher or protocol mismatch
If handshakes fail only on certain JDK distributions, check the cipher list configured on both ends. JDK distributions may use different cipher naming conventions, and hardcoded cipher overrides in some ZooKeeper versions can break negotiation on JDKs that use different prefixes.
If you control both ends, force a specific TLS protocol version (TLSv1.2 or TLSv1.3) on both client and server to eliminate negotiation ambiguity.
Hostname verification failure
This typically appears in Kubernetes or dynamic infrastructure where the certificate SAN entries do not match the hostname or IP the client used to connect. The fix is either to regenerate the certificate with the correct SANs or to disable hostname verification on the client, which weakens security and should only be a temporary stopgap.
For quorum TLS, hostname verification is enabled by default. In Kubernetes, reverse DNS lookups for pod IPs may return names that do not match the certificate SANs, breaking quorum formation.
Low host entropy
Install haveged or ensure the kernel RNG is properly seeded. On virtualized hosts, confirm that the hypervisor provides entropy via virtio-rng or an equivalent mechanism. Low entropy manifests as zk_tls_handshake_exceeded incrementing alongside slow or incomplete handshakes.
Prevention
- Monitor certificate expiry externally. ZooKeeper does not expose cert expiry as a metric. Use
openssl s_client, cert-manager in Kubernetes, or your PKI tooling to alert on certificates approaching expiry. Treat any cert within 30 days of expiry as a ticket. - Alert on
zk_unsuccessful_handshakerate. Any sustained non-zero rate in production warrants investigation. A single failing client is noisy; a fleet-wide spike is an outage. - Alert on
zk_non_mtls_remote_conn_countin mTLS-required environments. The count should be zero. Non-zero means a client bypassed mutual TLS. - Test TLS after every cert rotation. Automated renewal that updates the keystore but does not trigger a ZooKeeper reload leaves the old cert in memory. Verify the running process presents the new cert.
- Standardize the client TLS property. Document that
zookeeper.client.secure=trueis the correct property and thatzookeeper.ssl.client.enabledoes not work. This prevents the most common client-side error. - Verify entropy on all ZooKeeper hosts. Add
/proc/sys/kernel/random/entropy_availto host monitoring. Entropy starvation is a known cause of TLS handshake stalls.
How Netdata helps
Netdata collects ZooKeeper TLS counters at per-second resolution alongside the host-level metrics that explain them. The useful correlations during a handshake incident:
- Handshake failure rate versus cert rotation timing. A spike in
zk_unsuccessful_handshakeorzk_tls_handshake_exceededthat aligns with a deployment window or cert renewal event narrows the cause immediately. zk_non_mtls_remote_conn_countas a standalone metric. A non-mTLS connection in an mTLS-required environment is visible on the dashboard without parsing server logs.- Host entropy from
/proc/sys/kernel/random/entropy_availcollected by the Linux plugin. Correlatezk_tls_handshake_exceededwith entropy drops on the same host. zk_unsuccessful_handshakealongsidezk_num_alive_connectionsandzk_connection_drop_count. If connections drop during handshake but never establish a session, the problem is TLS, not session management or ephemeral timeouts.- JVM and GC metrics on the same host to rule out process-level stalls as a secondary cause of handshake timeouts.
Related guides
- ZooKeeper data size growing: using ZooKeeper as a database is an anti-pattern
- ZooKeeper autopurge not configured: snapshots and logs filling the disk over months
- ZooKeeper avg_latency hides write stalls: why the headline number lies
- ZooKeeper “Cannot open channel to N at election address”: the blocked election port
- ZooKeeper “Client session timed out, have not heard from server”: the heartbeat miss
- ZooKeeper connection drops spiking: sessions dying in bursts
- ZooKeeper KeeperErrorCode = ConnectionLoss: the transient disconnect every client hits
- ZooKeeper dataLogDir sharing a disk with snapshots: the #1 fsync-latency footgun
- ZooKeeper “Detected pause in JVM or host machine (eg GC)”: the pause-monitor warning
- ZooKeeper data tree digest mismatch: detecting corruption before it spreads
- ZooKeeper transaction log disk full: the crash with no graceful degradation
- ZooKeeper follower doing a SNAP sync: full snapshot transfer and its blast radius






