Pārlūkot izejas kodu

Allow adjustment of transport TLS handshake timeout (#130909)

The default 10s TLS handshake timeout may be too short if there is some
bug causing event-loop latency, and this has more serious consequences
than the underlying performance issue (e.g. it prevents the cluster from
scaling up to work around the problem). With this commit we expose a
setting that allows the timeout to be configured, providing a workaround
in such cases.
David Turner 3 mēneši atpakaļ
vecāks
revīzija
e57a0d0fa3

+ 5 - 0
docs/changelog/130909.yaml

@@ -0,0 +1,5 @@
+pr: 130909
+summary: Allow adjustment of transport TLS handshake timeout
+area: Network
+type: enhancement
+issues: []

+ 8 - 0
docs/reference/elasticsearch/configuration-reference/security-settings.md

@@ -1933,6 +1933,8 @@ You can configure the following TLS/SSL settings.
 `xpack.security.transport.ssl.trust_restrictions.x509_fields` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on Elastic Cloud Hosted")
 :   Specifies which field(s) from the TLS certificate is used to match for the restricted trust management that is used for remote clusters connections. This should only be set when a self managed cluster can not create certificates that follow the Elastic Cloud pattern. The default value is ["subjectAltName.otherName.commonName"], the Elastic Cloud pattern. "subjectAltName.dnsName" is also supported and can be configured in addition to or in replacement of the default.
 
+`xpack.security.transport.ssl.handshake_timeout`
+:   Specifies the timeout for a TLS handshake when opening a transport connection. Defaults to `10s`.
 
 ### Transport TLS/SSL key and trusted certificate settings [security-transport-tls-ssl-key-trusted-certificate-settings]
 
@@ -2131,6 +2133,9 @@ You can configure the following TLS/SSL settings.
 
     For more information, see Oracle’s [Java Cryptography Architecture documentation](https://docs.oracle.com/en/java/javase/11/security/oracle-providers.md#GUID-7093246A-31A3-4304-AC5F-5FB6400405E2).
 
+`xpack.security.remote_cluster_server.ssl.handshake_timeout`
+:   Specifies the timeout for a TLS handshake when handling an inbound remote-cluster connection. Defaults to `10s`.
+
 
 ### Remote cluster server (API key based model) TLS/SSL key and trusted certificate settings [security-remote-cluster-server-tls-ssl-key-trusted-certificate-settings]
 
@@ -2260,6 +2265,9 @@ You can configure the following TLS/SSL settings.
 
     For more information, see Oracle’s [Java Cryptography Architecture documentation](https://docs.oracle.com/en/java/javase/11/security/oracle-providers.md#GUID-7093246A-31A3-4304-AC5F-5FB6400405E2).
 
+`xpack.security.remote_cluster_client.ssl.handshake_timeout`
+:   Specifies the timeout for a TLS handshake when opening a remote-cluster connection. Defaults to `10s`.
+
 
 ### Remote cluster client (API key based model) TLS/SSL key and trusted certificate settings [security-remote-cluster-client-tls-ssl-key-trusted-certificate-settings]
 

+ 20 - 4
libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfiguration.java

@@ -40,7 +40,8 @@ public record SslConfiguration(
     SslVerificationMode verificationMode,
     SslClientAuthenticationMode clientAuth,
     List<String> ciphers,
-    List<String> supportedProtocols
+    List<String> supportedProtocols,
+    long handshakeTimeoutMillis
 ) {
 
     /**
@@ -71,7 +72,8 @@ public record SslConfiguration(
         SslVerificationMode verificationMode,
         SslClientAuthenticationMode clientAuth,
         List<String> ciphers,
-        List<String> supportedProtocols
+        List<String> supportedProtocols,
+        long handshakeTimeoutMillis
     ) {
         this.settingPrefix = settingPrefix;
         this.explicitlyConfigured = explicitlyConfigured;
@@ -85,6 +87,10 @@ public record SslConfiguration(
         this.keyConfig = Objects.requireNonNull(keyConfig, "key config cannot be null");
         this.verificationMode = Objects.requireNonNull(verificationMode, "verification mode cannot be null");
         this.clientAuth = Objects.requireNonNull(clientAuth, "client authentication cannot be null");
+        if (handshakeTimeoutMillis < 1L) {
+            throw new SslConfigException("handshake timeout must be at least 1ms");
+        }
+        this.handshakeTimeoutMillis = handshakeTimeoutMillis;
         this.ciphers = Collections.unmodifiableList(ciphers);
         this.supportedProtocols = Collections.unmodifiableList(supportedProtocols);
     }
@@ -164,11 +170,21 @@ public record SslConfiguration(
             && this.verificationMode == that.verificationMode
             && this.clientAuth == that.clientAuth
             && Objects.equals(this.ciphers, that.ciphers)
-            && Objects.equals(this.supportedProtocols, that.supportedProtocols);
+            && Objects.equals(this.supportedProtocols, that.supportedProtocols)
+            && this.handshakeTimeoutMillis == that.handshakeTimeoutMillis;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(settingPrefix, trustConfig, keyConfig, verificationMode, clientAuth, ciphers, supportedProtocols);
+        return Objects.hash(
+            settingPrefix,
+            trustConfig,
+            keyConfig,
+            verificationMode,
+            clientAuth,
+            ciphers,
+            supportedProtocols,
+            handshakeTimeoutMillis
+        );
     }
 }

+ 4 - 0
libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java

@@ -132,6 +132,10 @@ public class SslConfigurationKeys {
      * The use of this setting {@link #isDeprecated(String) is deprecated}.
      */
     public static final String KEY_LEGACY_PASSPHRASE = "key_passphrase";
+    /**
+     * The timeout for TLS handshakes in this context.
+     */
+    public static final String HANDSHAKE_TIMEOUT = "handshake_timeout";
 
     private static final Set<String> DEPRECATED_KEYS = new HashSet<>(
         Arrays.asList(TRUSTSTORE_LEGACY_PASSWORD, KEYSTORE_LEGACY_PASSWORD, KEYSTORE_LEGACY_KEY_PASSWORD, KEY_LEGACY_PASSPHRASE)

+ 11 - 1
libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java

@@ -10,6 +10,7 @@
 package org.elasticsearch.common.ssl;
 
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.TimeValue;
 
 import java.nio.file.Path;
 import java.security.KeyStore;
@@ -27,6 +28,7 @@ import static org.elasticsearch.common.ssl.SslConfigurationKeys.CERTIFICATE;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.CERTIFICATE_AUTHORITIES;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.CIPHERS;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.CLIENT_AUTH;
+import static org.elasticsearch.common.ssl.SslConfigurationKeys.HANDSHAKE_TIMEOUT;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEY;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_ALGORITHM;
 import static org.elasticsearch.common.ssl.SslConfigurationKeys.KEYSTORE_LEGACY_KEY_PASSWORD;
@@ -152,6 +154,8 @@ public abstract class SslConfigurationLoader {
     private static final char[] EMPTY_PASSWORD = new char[0];
     public static final List<X509Field> GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS = List.of(X509Field.SAN_OTHERNAME_COMMONNAME);
 
+    public static final TimeValue DEFAULT_HANDSHAKE_TIMEOUT = TimeValue.timeValueSeconds(10);
+
     private final String settingPrefix;
 
     private SslTrustConfig defaultTrustConfig;
@@ -302,6 +306,11 @@ public abstract class SslConfigurationLoader {
             X509Field::parseForRestrictedTrust,
             defaultRestrictedTrustFields
         );
+        final long handshakeTimeoutMillis = resolveSetting(
+            HANDSHAKE_TIMEOUT,
+            s -> TimeValue.parseTimeValue(s, HANDSHAKE_TIMEOUT),
+            DEFAULT_HANDSHAKE_TIMEOUT
+        ).millis();
 
         final SslKeyConfig keyConfig = buildKeyConfig(basePath);
         final SslTrustConfig trustConfig = buildTrustConfig(basePath, verificationMode, keyConfig, Set.copyOf(trustRestrictionsX509Fields));
@@ -321,7 +330,8 @@ public abstract class SslConfigurationLoader {
             verificationMode,
             clientAuth,
             ciphers,
-            protocols
+            protocols,
+            handshakeTimeoutMillis
         );
     }
 

+ 38 - 10
libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslConfigurationTests.java

@@ -39,6 +39,7 @@ public class SslConfigurationTests extends ESTestCase {
         final SslClientAuthenticationMode clientAuth = randomFrom(SslClientAuthenticationMode.values());
         final List<String> ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size()), DEFAULT_CIPHERS);
         final List<String> protocols = randomSubsetOf(randomIntBetween(1, 4), VALID_PROTOCOLS);
+        final long handshakeTimeoutMillis = randomHandshakeTimeoutMillis();
         final SslConfiguration configuration = new SslConfiguration(
             "test.ssl",
             true,
@@ -47,7 +48,8 @@ public class SslConfigurationTests extends ESTestCase {
             verificationMode,
             clientAuth,
             ciphers,
-            protocols
+            protocols,
+            handshakeTimeoutMillis
         );
 
         assertThat(configuration.trustConfig(), is(trustConfig));
@@ -56,6 +58,7 @@ public class SslConfigurationTests extends ESTestCase {
         assertThat(configuration.clientAuth(), is(clientAuth));
         assertThat(configuration.getCipherSuites(), is(ciphers));
         assertThat(configuration.supportedProtocols(), is(protocols));
+        assertThat(configuration.handshakeTimeoutMillis(), is(handshakeTimeoutMillis));
 
         assertThat(configuration.toString(), containsString("TEST-TRUST"));
         assertThat(configuration.toString(), containsString("TEST-KEY"));
@@ -63,6 +66,7 @@ public class SslConfigurationTests extends ESTestCase {
         assertThat(configuration.toString(), containsString(clientAuth.toString()));
         assertThat(configuration.toString(), containsString(randomFrom(ciphers)));
         assertThat(configuration.toString(), containsString(randomFrom(protocols)));
+        assertThat(configuration.toString(), containsString("handshakeTimeoutMillis=" + handshakeTimeoutMillis));
     }
 
     public void testEqualsAndHashCode() {
@@ -72,6 +76,7 @@ public class SslConfigurationTests extends ESTestCase {
         final SslClientAuthenticationMode clientAuth = randomFrom(SslClientAuthenticationMode.values());
         final List<String> ciphers = randomSubsetOf(randomIntBetween(1, DEFAULT_CIPHERS.size() - 1), DEFAULT_CIPHERS);
         final List<String> protocols = randomSubsetOf(randomIntBetween(1, VALID_PROTOCOLS.length - 1), VALID_PROTOCOLS);
+        final long handshakeTimeoutMillis = randomHandshakeTimeoutMillis();
         final SslConfiguration configuration = new SslConfiguration(
             "test.ssl",
             true,
@@ -80,7 +85,8 @@ public class SslConfigurationTests extends ESTestCase {
             verificationMode,
             clientAuth,
             ciphers,
-            protocols
+            protocols,
+            handshakeTimeoutMillis
         );
 
         EqualsHashCodeTestUtils.checkEqualsAndHashCode(
@@ -93,14 +99,15 @@ public class SslConfigurationTests extends ESTestCase {
                 orig.verificationMode(),
                 orig.clientAuth(),
                 orig.getCipherSuites(),
-                orig.supportedProtocols()
+                orig.supportedProtocols(),
+                orig.handshakeTimeoutMillis()
             ),
             this::mutateSslConfiguration
         );
     }
 
     private SslConfiguration mutateSslConfiguration(SslConfiguration orig) {
-        return switch (randomIntBetween(1, 4)) {
+        return switch (randomIntBetween(1, 5)) {
             case 1 -> new SslConfiguration(
                 "test.ssl",
                 true,
@@ -109,7 +116,8 @@ public class SslConfigurationTests extends ESTestCase {
                 randomValueOtherThan(orig.verificationMode(), () -> randomFrom(SslVerificationMode.values())),
                 orig.clientAuth(),
                 orig.getCipherSuites(),
-                orig.supportedProtocols()
+                orig.supportedProtocols(),
+                orig.handshakeTimeoutMillis()
             );
             case 2 -> new SslConfiguration(
                 "test.ssl",
@@ -119,7 +127,8 @@ public class SslConfigurationTests extends ESTestCase {
                 orig.verificationMode(),
                 randomValueOtherThan(orig.clientAuth(), () -> randomFrom(SslClientAuthenticationMode.values())),
                 orig.getCipherSuites(),
-                orig.supportedProtocols()
+                orig.supportedProtocols(),
+                orig.handshakeTimeoutMillis()
             );
             case 3 -> new SslConfiguration(
                 "test.ssl",
@@ -129,7 +138,19 @@ public class SslConfigurationTests extends ESTestCase {
                 orig.verificationMode(),
                 orig.clientAuth(),
                 DEFAULT_CIPHERS,
-                orig.supportedProtocols()
+                orig.supportedProtocols(),
+                orig.handshakeTimeoutMillis()
+            );
+            case 4 -> new SslConfiguration(
+                "test.ssl",
+                true,
+                orig.trustConfig(),
+                orig.keyConfig(),
+                orig.verificationMode(),
+                orig.clientAuth(),
+                orig.getCipherSuites(),
+                Arrays.asList(VALID_PROTOCOLS),
+                orig.handshakeTimeoutMillis()
             );
             default -> new SslConfiguration(
                 "test.ssl",
@@ -139,11 +160,16 @@ public class SslConfigurationTests extends ESTestCase {
                 orig.verificationMode(),
                 orig.clientAuth(),
                 orig.getCipherSuites(),
-                Arrays.asList(VALID_PROTOCOLS)
+                orig.supportedProtocols(),
+                randomValueOtherThan(orig.handshakeTimeoutMillis(), SslConfigurationTests::randomHandshakeTimeoutMillis)
             );
         };
     }
 
+    private static long randomHandshakeTimeoutMillis() {
+        return randomLongBetween(1, 100000);
+    }
+
     public void testDependentFiles() {
         final SslTrustConfig trustConfig = Mockito.mock(SslTrustConfig.class);
         final SslKeyConfig keyConfig = Mockito.mock(SslKeyConfig.class);
@@ -155,7 +181,8 @@ public class SslConfigurationTests extends ESTestCase {
             randomFrom(SslVerificationMode.values()),
             randomFrom(SslClientAuthenticationMode.values()),
             DEFAULT_CIPHERS,
-            SslConfigurationLoader.DEFAULT_PROTOCOLS
+            SslConfigurationLoader.DEFAULT_PROTOCOLS,
+            randomHandshakeTimeoutMillis()
         );
 
         final Path dir = createTempDir();
@@ -182,7 +209,8 @@ public class SslConfigurationTests extends ESTestCase {
             randomFrom(SslVerificationMode.values()),
             randomFrom(SslClientAuthenticationMode.values()),
             DEFAULT_CIPHERS,
-            Collections.singletonList(protocol)
+            Collections.singletonList(protocol),
+            randomHandshakeTimeoutMillis()
         );
 
         Mockito.when(trustConfig.createTrustManager()).thenReturn(null);

+ 3 - 7
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

@@ -344,18 +344,14 @@ public class XPackSettings {
     public static final SslClientAuthenticationMode REMOTE_CLUSTER_CLIENT_AUTH_DEFAULT = SslClientAuthenticationMode.NONE;
     public static final SslVerificationMode VERIFICATION_MODE_DEFAULT = SslVerificationMode.FULL;
 
-    // http specific settings
     public static final String HTTP_SSL_PREFIX = SecurityField.setting("http.ssl.");
-    private static final SSLConfigurationSettings HTTP_SSL = SSLConfigurationSettings.withPrefix(HTTP_SSL_PREFIX, true);
-
-    // transport specific settings
     public static final String TRANSPORT_SSL_PREFIX = SecurityField.setting("transport.ssl.");
-    private static final SSLConfigurationSettings TRANSPORT_SSL = SSLConfigurationSettings.withPrefix(TRANSPORT_SSL_PREFIX, true);
-
-    // remote cluster specific settings
     public static final String REMOTE_CLUSTER_SERVER_SSL_PREFIX = SecurityField.setting("remote_cluster_server.ssl.");
     public static final String REMOTE_CLUSTER_CLIENT_SSL_PREFIX = SecurityField.setting("remote_cluster_client.ssl.");
 
+    private static final SSLConfigurationSettings HTTP_SSL = SSLConfigurationSettings.withPrefix(HTTP_SSL_PREFIX, true);
+    private static final SSLConfigurationSettings TRANSPORT_SSL = SSLConfigurationSettings.withPrefix(TRANSPORT_SSL_PREFIX, true);
+
     private static final SSLConfigurationSettings REMOTE_CLUSTER_SERVER_SSL = SSLConfigurationSettings.withPrefix(
         REMOTE_CLUSTER_SERVER_SSL_PREFIX,
         false

+ 23 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java

@@ -13,9 +13,12 @@ import org.elasticsearch.common.settings.Setting.Property;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.ssl.SslClientAuthenticationMode;
 import org.elasticsearch.common.ssl.SslConfigurationKeys;
+import org.elasticsearch.common.ssl.SslConfigurationLoader;
 import org.elasticsearch.common.ssl.SslVerificationMode;
 import org.elasticsearch.common.ssl.X509Field;
 import org.elasticsearch.common.util.CollectionUtils;
+import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.security.authc.RealmConfig;
 
 import java.util.ArrayList;
@@ -30,6 +33,7 @@ import java.util.stream.Collectors;
 import javax.net.ssl.TrustManagerFactory;
 
 import static org.elasticsearch.common.ssl.SslConfigurationLoader.GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS;
+import static org.elasticsearch.xpack.core.XPackSettings.TRANSPORT_SSL_PREFIX;
 
 /**
  * Bridges SSLConfiguration into the {@link Settings} framework, using {@link Setting} objects.
@@ -50,6 +54,7 @@ public class SSLConfigurationSettings {
     final Setting<List<String>> caPaths;
     final Setting<Optional<SslClientAuthenticationMode>> clientAuth;
     final Setting<Optional<SslVerificationMode>> verificationMode;
+    final Setting<TimeValue> handshakeTimeout;
 
     // public for PKI realm
     private final Setting<SecureString> legacyTruststorePassword;
@@ -223,6 +228,11 @@ public class SSLConfigurationSettings {
     public static final Function<String, Setting.AffixSetting<Optional<SslVerificationMode>>> VERIFICATION_MODE_SETTING_REALM =
         VERIFICATION_MODE::realm;
 
+    public static final SslSetting<TimeValue> HANDSHAKE_TIMEOUT = SslSetting.setting(
+        SslConfigurationKeys.HANDSHAKE_TIMEOUT,
+        key -> Setting.positiveTimeSetting(key, SslConfigurationLoader.DEFAULT_HANDSHAKE_TIMEOUT, Property.NodeScope)
+    );
+
     /**
      * @param prefix The prefix under which each setting should be defined. Must be either the empty string (<code>""</code>) or a string
      *               ending in <code>"."</code>
@@ -246,6 +256,7 @@ public class SSLConfigurationSettings {
         caPaths = CERT_AUTH_PATH.withPrefix(prefix);
         clientAuth = CLIENT_AUTH_SETTING.withPrefix(prefix);
         verificationMode = VERIFICATION_MODE.withPrefix(prefix);
+        handshakeTimeout = HANDSHAKE_TIMEOUT.withPrefix(prefix);
 
         final List<Setting<? extends Object>> enabled = CollectionUtils.arrayAsArrayList(
             ciphers,
@@ -270,6 +281,16 @@ public class SSLConfigurationSettings {
         enabled.addAll(x509KeyPair.getEnabledSettings());
         disabled.addAll(x509KeyPair.getDisabledSettings());
 
+        if (TRANSPORT_SSL_PREFIX.equals(prefix)
+            || XPackSettings.REMOTE_CLUSTER_CLIENT_SSL_PREFIX.equals(prefix)
+            || XPackSettings.REMOTE_CLUSTER_SERVER_SSL_PREFIX.equals(prefix)) {
+            enabled.add(handshakeTimeout);
+        } else {
+            // Today the handshake timeout is only adjustable for transport connections - see SecurityNetty4Transport. In principle we
+            // could extend this to other contexts too, we just haven't done so yet.
+            disabled.add(handshakeTimeout);
+        }
+
         this.enabledSettings = Collections.unmodifiableList(enabled);
         this.disabledSettings = Collections.unmodifiableList(disabled);
     }
@@ -327,7 +348,8 @@ public class SSLConfigurationSettings {
             CERT,
             CERT_AUTH_PATH,
             CLIENT_AUTH_SETTING,
-            VERIFICATION_MODE
+            VERIFICATION_MODE,
+            HANDSHAKE_TIMEOUT
         );
     }
 

+ 2 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java

@@ -240,6 +240,7 @@ public class SecurityNetty4Transport extends Netty4Transport {
             SSLEngine serverEngine = sslService.createSSLEngine(configuration, null, -1);
             serverEngine.setUseClientMode(false);
             final SslHandler sslHandler = new SslHandler(serverEngine);
+            sslHandler.setHandshakeTimeoutMillis(configuration.handshakeTimeoutMillis());
             ch.pipeline().addFirst("sslhandler", sslHandler);
             super.initChannel(ch);
             assert ch.pipeline().first() == sslHandler : "SSL handler must be first handler in pipeline";
@@ -340,6 +341,7 @@ public class SecurityNetty4Transport extends Netty4Transport {
             }
             final ChannelPromise connectPromise = ctx.newPromise();
             final SslHandler sslHandler = new SslHandler(sslEngine);
+            sslHandler.setHandshakeTimeoutMillis(sslConfiguration.handshakeTimeoutMillis());
             ctx.pipeline().replace(this, "ssl", sslHandler);
             final Future<?> handshakePromise = sslHandler.handshakeFuture();
             Netty4Utils.addListener(connectPromise, result -> {

+ 6 - 3
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java

@@ -1118,7 +1118,8 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
                 randomFrom(SslVerificationMode.values()),
                 SslClientAuthenticationMode.REQUIRED,
                 List.of("TLS_AES_256_GCM_SHA384"),
-                List.of("TLSv1.3")
+                List.of("TLSv1.3"),
+                randomLongBetween(1, 100000)
             )
         );
 
@@ -1131,7 +1132,8 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
                 randomFrom(SslVerificationMode.values()),
                 SslClientAuthenticationMode.NONE,
                 List.of(Runtime.version().feature() < 24 ? "TLS_RSA_WITH_AES_256_GCM_SHA384" : "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"),
-                List.of("TLSv1.2")
+                List.of("TLSv1.2"),
+                randomLongBetween(1, 100000)
             )
         );
         doThrow(new AssertionError("profile filters should not be configured for remote cluster client")).when(sslService)
@@ -1181,7 +1183,8 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase {
                 randomFrom(SslVerificationMode.values()),
                 SslClientAuthenticationMode.REQUIRED,
                 List.of("TLS_AES_256_GCM_SHA384"),
-                List.of("TLSv1.3")
+                List.of("TLSv1.3"),
+                randomLongBetween(1, 100000)
             )
         );
         doThrow(new AssertionError("profile filters should not be configured for remote cluster server when the port is disabled")).when(

+ 59 - 5
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SimpleSecurityNetty4ServerTransportTests.java

@@ -11,6 +11,7 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.socket.nio.NioChannelOption;
 import io.netty.handler.ssl.SslHandshakeTimeoutException;
 
+import org.apache.logging.log4j.Level;
 import org.apache.lucene.util.Constants;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.TransportVersion;
@@ -35,6 +36,7 @@ import org.elasticsearch.common.transport.TransportAddress;
 import org.elasticsearch.common.unit.ByteSizeValue;
 import org.elasticsearch.common.util.PageCacheRecycler;
 import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.Releasable;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.core.TimeValue;
@@ -42,6 +44,9 @@ import org.elasticsearch.env.TestEnvironment;
 import org.elasticsearch.indices.breaker.CircuitBreakerService;
 import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
 import org.elasticsearch.mocksocket.MockServerSocket;
+import org.elasticsearch.mocksocket.MockSocket;
+import org.elasticsearch.test.MockLog;
+import org.elasticsearch.test.junit.annotations.TestLogging;
 import org.elasticsearch.test.transport.MockTransportService;
 import org.elasticsearch.test.transport.StubbableTransport;
 import org.elasticsearch.threadpool.ThreadPool;
@@ -65,6 +70,7 @@ import org.elasticsearch.xpack.security.authc.CrossClusterAccessAuthenticationSe
 import org.elasticsearch.xpack.security.transport.SSLEngineUtils;
 import org.elasticsearch.xpack.security.transport.filter.IPFilter;
 
+import java.io.EOFException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.InetAddress;
@@ -902,7 +908,15 @@ public class SimpleSecurityNetty4ServerTransportTests extends AbstractSimpleTran
         }
     }
 
+    @TestLogging(reason = "inbound timeout is reported at TRACE", value = "org.elasticsearch.transport.netty4.ESLoggingHandler:TRACE")
     public void testTlsHandshakeTimeout() throws IOException {
+        runOutboundTlsHandshakeTimeoutTest(null);
+        runOutboundTlsHandshakeTimeoutTest(randomLongBetween(1, 500));
+        runInboundTlsHandshakeTimeoutTest(null);
+        runInboundTlsHandshakeTimeoutTest(randomLongBetween(1, 500));
+    }
+
+    private void runOutboundTlsHandshakeTimeoutTest(@Nullable /* to use the default */ Long handshakeTimeoutMillis) throws IOException {
         final CountDownLatch doneLatch = new CountDownLatch(1);
         try (ServerSocket socket = new MockServerSocket()) {
             socket.bind(getLocalEphemeral(), 1);
@@ -928,16 +942,56 @@ public class SimpleSecurityNetty4ServerTransportTests extends AbstractSimpleTran
                 TransportRequestOptions.Type.REG,
                 TransportRequestOptions.Type.STATE
             );
-            final var future = new TestPlainActionFuture<Releasable>();
-            serviceA.connectToNode(dummy, builder.build(), future);
-            final var ex = expectThrows(ExecutionException.class, ConnectTransportException.class, future::get); // long wait
-            assertEquals("[][" + dummy.getAddress() + "] connect_exception", ex.getMessage());
-            assertNotNull(ExceptionsHelper.unwrap(ex, SslHandshakeTimeoutException.class));
+            final ConnectTransportException exception;
+            final var transportSettings = Settings.builder();
+            if (handshakeTimeoutMillis == null) {
+                handshakeTimeoutMillis = 10000L; // default
+            } else {
+                transportSettings.put("xpack.security.transport.ssl.handshake_timeout", TimeValue.timeValueMillis(handshakeTimeoutMillis));
+            }
+            try (var service = buildService(getTestName(), version0, transportVersion0, transportSettings.build())) {
+                final var future = new TestPlainActionFuture<Releasable>();
+                service.connectToNode(dummy, builder.build(), future);
+                exception = expectThrows(ExecutionException.class, ConnectTransportException.class, future::get); // long wait
+                assertEquals("[][" + dummy.getAddress() + "] connect_exception", exception.getMessage());
+                assertThat(
+                    asInstanceOf(SslHandshakeTimeoutException.class, exception.getCause()).getMessage(),
+                    equalTo("handshake timed out after " + handshakeTimeoutMillis + "ms")
+                );
+            }
         } finally {
             doneLatch.countDown();
         }
     }
 
+    @SuppressForbidden(reason = "test needs a simple TCP connection")
+    private void runInboundTlsHandshakeTimeoutTest(@Nullable /* to use the default */ Long handshakeTimeoutMillis) throws IOException {
+        final var transportSettings = Settings.builder();
+        if (handshakeTimeoutMillis == null) {
+            handshakeTimeoutMillis = 10000L; // default
+        } else {
+            transportSettings.put("xpack.security.transport.ssl.handshake_timeout", TimeValue.timeValueMillis(handshakeTimeoutMillis));
+        }
+        try (
+            var service = buildService(getTestName(), version0, transportVersion0, transportSettings.build());
+            Socket clientSocket = new MockSocket();
+            MockLog mockLog = MockLog.capture("org.elasticsearch.transport.netty4.ESLoggingHandler")
+        ) {
+            mockLog.addExpectation(
+                new MockLog.SeenEventExpectation(
+                    "timeout event message",
+                    "org.elasticsearch.transport.netty4.ESLoggingHandler",
+                    Level.TRACE,
+                    "SslHandshakeTimeoutException: handshake timed out after " + handshakeTimeoutMillis + "ms"
+                )
+            );
+
+            clientSocket.connect(service.boundAddress().boundAddresses()[0].address());
+            expectThrows(EOFException.class, () -> clientSocket.getInputStream().skipNBytes(Long.MAX_VALUE));
+            mockLog.assertAllExpectationsMatched();
+        }
+    }
+
     public void testTcpHandshakeConnectionReset() throws IOException, InterruptedException {
         assumeFalse("Can't run in a FIPS JVM, TrustAllConfig is not a SunJSSE TrustManagers", inFipsJvm());
         SSLService sslService = createSSLService();