Skip to content

[rpc] Wire TLS encryption into the client Netty pipeline - #5136

Open
morazow wants to merge 1 commit into
apache:mainfrom
morazow:tls-client-pipeline-3797
Open

morazow wants to merge 1 commit into
apache:mainfrom
morazow:tls-client-pipeline-3797

Conversation

@morazow

@morazow morazow commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Purpose

Linked issue: close #3797

Part of #3786 (FIP-29: (m)TLS Support). Builds on #3796 (SSL config and SslContextFactory) and #3792 (server pipeline).

Before this change, client.security.ssl.enabled=true had no effect: the client still sent plaintext. Against a TLS listener the request waited for the server handshake timeout (10 seconds by default) and then failed with a retriable "Disconnected" error.

Brief change log

  • NettyClient builds the client SslContext once, so an invalid keystore or truststore fails when the client is created, not on every connect.
  • ServerConnection adds the SslHandler first in the pipeline after the connect succeeds. ClientChannelInitializer runs before the connect and does not know the remote host. Hostname verification and Server Name Indication (SNI) use the advertised host of the ServerNode, not the resolved address.
  • NettyClientHandler handles SslHandshakeCompletionEvent. A handshake that fails with an SSLException (untrusted certificate, hostname mismatch, plaintext listener) closes the connection with a non-retriable AuthenticationException. Without this, the pending ApiVersions write failed with the SSLException, an IOException, which became a retriable NetworkException, so the client retried until it timed out. A handshake timeout and a channel closed during the handshake stay retriable.
  • ServerConnection.TlsRecordCheck checks the first byte from the server. SslHandler does not detect a plaintext listener by itself: the SERVER_FAILURE frame that the server answers with starts with 00 00 00 xx 02, which Netty reads as the header of an incomplete SSLv2 record. It then waits for more data, and the handshake fails only when the server closes the connection, with a ClosedChannelException that looks like a server restart. A TLS record always starts with a content type from 20 to 24. The check fails the handshake on any other first byte and then removes itself from the pipeline.
  • FlussProtocolPlugin: servers create their RpcClient from the server configuration, so client.security.ssl.* in server.yaml controls the connections between servers. The check that rejected TLS on the internal listener is replaced by a consistency rule: the internal listener is listed in security.ssl.enabled.listeners exactly when client.security.ssl.enabled is true. The rule runs even without any server TLS configuration, and a mismatch in either direction fails at startup. [TLS] Add zero-downtime internal listener migration (optional-SSL) #3798 can relax it for rolling migration.
  • FlussClusterExtension copies client.security.ssl.* from the cluster configuration into its helper RpcClient, which talks to the internal listener while the cluster starts.

Tests

  • NettyClientTlsTest (new): a request over TLS with a negotiated protocol and cipher suite; a TLS client against a plaintext listener fails fast with AuthenticationException; an untrusted server certificate fails with AuthenticationException; a hostname mismatch fails under https and succeeds with an empty algorithm; an invalid client TLS configuration fails in the NettyClient constructor. Without the change, the three failure cases fail with the retriable NetworkException.
  • NettyServerTlsTest: uses the real NettyClient instead of a hand-built Netty Bootstrap.
  • FlussProtocolPluginTest: the internal-listener rule in both directions, and the matching case.
  • TlsDataPathITCase (new): a cluster of 3 tablet servers with replication factor 3, whose only listener (also the internal listener) uses TLS. It appends and scans 2,000 rows of 1 KiB in a log table, and upserts and looks up rows in a primary-key table. With acks all, the writes complete only after the followers fetch them, and the test checks that all 3 replicas stay in sync.

Note: without advertised.listeners, a server advertises its bound IP address, so the ITCase disables hostname verification. NettyClientTlsTest covers hostname verification.

API and Format

No API or storage format change. One startup rule is new: a server now rejects a mismatch between the internal listener's TLS setting and client.security.ssl.enabled.

Documentation

Updated the description of client.security.ssl.enabled. The user guide for TLS belongs to the umbrella docs ticket.

🤖 Generated with Claude Code

@morazow
morazow force-pushed the tls-client-pipeline-3797 branch 6 times, most recently from 41967c8 to 443b9e8 Compare November 11, 2026 14:10
With client.security.ssl.enabled the NettyClient now builds the client
SslContext once and adds an SslHandler to each connection after the
connect succeeds, verifying the advertised host name of the server.

A failed TLS handshake (untrusted certificate, hostname mismatch,
plaintext listener) is reported as a non-retriable
AuthenticationException instead of a retriable NetworkException, so the
client fails at once rather than retrying until it times out. A
handshake timeout and a connection closed during the handshake stay
retriable.

The SslHandler does not detect a plaintext listener by itself: the
SERVER_FAILURE frame the server answers with reads as an incomplete
SSLv2 record header, so the handshake only failed once the server
closed the connection. A check on the first byte from the server
reports this case as non-retriable too.

Servers create their RpcClient from the server configuration, so
client.security.ssl.enabled now controls the connections between
servers. The check that rejected TLS on the internal listener is
replaced by one that requires the internal listener to be TLS-enabled
exactly when client.security.ssl.enabled is true.

After such a non-retriable authentication failure the closed connection
stays in place for one second, so requests fail at once with the same
cause instead of repeating the failing handshake on every request. This
also applies to a non-retriable SASL authentication failure.

client.connect-timeout now also bounds the TLS handshake, which
previously used the fixed 10 second default of the SslHandler. A
handshake that does not complete logs a warning, and a successful one
logs the negotiated protocol and cipher suite at debug level.
@morazow
morazow force-pushed the tls-client-pipeline-3797 branch from 443b9e8 to 9e7ba87 Compare November 11, 2026 14:26
@morazow
morazow marked this pull request as ready for review November 11, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TLS] Wire TLS encryption into the client Netty pipeline

1 participant