Skip to content

Commit f44ebec

Browse files
committed
Allow adjustment of transport TLS handshake timeout
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.
1 parent 4659e89 commit f44ebec

File tree

4 files changed

+80
-6
lines changed

4 files changed

+80
-6
lines changed

docs/reference/elasticsearch/configuration-reference/security-settings.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,8 @@ You can configure the following TLS/SSL settings.
19331933
`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")
19341934
: 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.
19351935

1936+
`xpack.security.transport.ssl.handshake_timeout`
1937+
: Specifies the timeout for a TLS handshake when opening a transport connection. Defaults to `10s`.
19361938

19371939
### Transport TLS/SSL key and trusted certificate settings [security-transport-tls-ssl-key-trusted-certificate-settings]
19381940

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.elasticsearch.common.ssl.SslTrustConfig;
3030
import org.elasticsearch.common.util.Maps;
3131
import org.elasticsearch.common.util.set.Sets;
32+
import org.elasticsearch.core.TimeValue;
3233
import org.elasticsearch.env.Environment;
3334
import org.elasticsearch.xpack.core.XPackSettings;
3435
import org.elasticsearch.xpack.core.common.socket.SocketAccess;
@@ -118,9 +119,16 @@ public class SSLService {
118119
Setting.Property.NodeScope
119120
);
120121

122+
private static final Setting<TimeValue> TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING = Setting.positiveTimeSetting(
123+
"xpack.security.transport.ssl.handshake_timeout",
124+
TimeValue.timeValueSeconds(10),
125+
Setting.Property.NodeScope
126+
);
127+
121128
private final Environment env;
122129
private final Settings settings;
123130
private final boolean diagnoseTrustExceptions;
131+
private final long handshakeTimeoutMillis;
124132

125133
/**
126134
* This is a mapping from "context name" (in general use, the name of a setting key)
@@ -156,6 +164,7 @@ public SSLService(Environment environment, Map<String, SslConfiguration> sslConf
156164
this.env = environment;
157165
this.settings = env.settings();
158166
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings());
167+
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(environment.settings()).millis();
159168
this.sslConfigurations = sslConfigurations;
160169
this.sslContexts = loadSslConfigurations(this.sslConfigurations);
161170
}
@@ -166,6 +175,7 @@ public SSLService(Settings settings, Environment environment) {
166175
this.env = environment;
167176
this.settings = env.settings();
168177
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(settings);
178+
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(settings).millis();
169179
this.sslConfigurations = getSSLConfigurations(env, this.settings);
170180
this.sslContexts = loadSslConfigurations(this.sslConfigurations);
171181
}
@@ -178,6 +188,7 @@ private SSLService(
178188
this.env = environment;
179189
this.settings = env.settings();
180190
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings());
191+
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(environment.settings()).millis();
181192
this.sslConfigurations = sslConfigurations;
182193
this.sslContexts = sslContexts;
183194
}
@@ -214,6 +225,7 @@ SSLContextHolder sslContextHolder(SslConfiguration sslConfiguration) {
214225

215226
public static void registerSettings(List<Setting<?>> settingList) {
216227
settingList.add(DIAGNOSE_TRUST_EXCEPTIONS_SETTING);
228+
settingList.add(TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING);
217229
}
218230

219231
/**
@@ -979,4 +991,8 @@ private static String sslContextAlgorithm(List<String> supportedProtocols) {
979991
"no supported SSL/TLS protocol was found in the configured supported protocols: " + supportedProtocols
980992
);
981993
}
994+
995+
public long getHandshakeTimeoutMillis() {
996+
return handshakeTimeoutMillis;
997+
}
982998
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ protected void initChannel(Channel ch) throws Exception {
240240
SSLEngine serverEngine = sslService.createSSLEngine(configuration, null, -1);
241241
serverEngine.setUseClientMode(false);
242242
final SslHandler sslHandler = new SslHandler(serverEngine);
243+
sslHandler.setHandshakeTimeoutMillis(sslService.getHandshakeTimeoutMillis());
243244
ch.pipeline().addFirst("sslhandler", sslHandler);
244245
super.initChannel(ch);
245246
assert ch.pipeline().first() == sslHandler : "SSL handler must be first handler in pipeline";
@@ -340,6 +341,7 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock
340341
}
341342
final ChannelPromise connectPromise = ctx.newPromise();
342343
final SslHandler sslHandler = new SslHandler(sslEngine);
344+
sslHandler.setHandshakeTimeoutMillis(sslService.getHandshakeTimeoutMillis());
343345
ctx.pipeline().replace(this, "ssl", sslHandler);
344346
final Future<?> handshakePromise = sslHandler.handshakeFuture();
345347
Netty4Utils.addListener(connectPromise, result -> {

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import io.netty.bootstrap.Bootstrap;
1010
import io.netty.channel.ChannelOption;
1111
import io.netty.channel.socket.nio.NioChannelOption;
12-
import io.netty.handler.ssl.SslHandshakeTimeoutException;
1312

13+
import org.apache.logging.log4j.Level;
1414
import org.apache.lucene.util.Constants;
1515
import org.elasticsearch.ExceptionsHelper;
1616
import org.elasticsearch.TransportVersion;
@@ -35,13 +35,17 @@
3535
import org.elasticsearch.common.unit.ByteSizeValue;
3636
import org.elasticsearch.common.util.PageCacheRecycler;
3737
import org.elasticsearch.core.IOUtils;
38+
import org.elasticsearch.core.Nullable;
3839
import org.elasticsearch.core.Releasable;
3940
import org.elasticsearch.core.SuppressForbidden;
4041
import org.elasticsearch.core.TimeValue;
4142
import org.elasticsearch.env.TestEnvironment;
4243
import org.elasticsearch.indices.breaker.CircuitBreakerService;
4344
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
4445
import org.elasticsearch.mocksocket.MockServerSocket;
46+
import org.elasticsearch.mocksocket.MockSocket;
47+
import org.elasticsearch.test.MockLog;
48+
import org.elasticsearch.test.junit.annotations.TestLogging;
4549
import org.elasticsearch.test.transport.MockTransportService;
4650
import org.elasticsearch.test.transport.StubbableTransport;
4751
import org.elasticsearch.threadpool.ThreadPool;
@@ -65,6 +69,7 @@
6569
import org.elasticsearch.xpack.security.transport.SSLEngineUtils;
6670
import org.elasticsearch.xpack.security.transport.filter.IPFilter;
6771

72+
import java.io.EOFException;
6873
import java.io.IOException;
6974
import java.io.UncheckedIOException;
7075
import java.net.InetAddress;
@@ -94,6 +99,7 @@
9499
import javax.net.ssl.SNIServerName;
95100
import javax.net.ssl.SSLContext;
96101
import javax.net.ssl.SSLEngine;
102+
import javax.net.ssl.SSLHandshakeException;
97103
import javax.net.ssl.SSLParameters;
98104
import javax.net.ssl.SSLServerSocket;
99105
import javax.net.ssl.SSLServerSocketFactory;
@@ -902,7 +908,15 @@ public void testTcpHandshakeTimeout() throws IOException {
902908
}
903909
}
904910

911+
@TestLogging(reason = "inbound timeout is reported at TRACE", value = "org.elasticsearch.transport.netty4.ESLoggingHandler:TRACE")
905912
public void testTlsHandshakeTimeout() throws IOException {
913+
runOutboundTlsHandshakeTimeoutTest(null);
914+
runOutboundTlsHandshakeTimeoutTest(randomLongBetween(1, 500));
915+
runInboundTlsHandshakeTimeoutTest(null);
916+
runInboundTlsHandshakeTimeoutTest(randomLongBetween(1, 500));
917+
}
918+
919+
private void runOutboundTlsHandshakeTimeoutTest(@Nullable /* to use the default */ Long handshakeTimeoutMillis) throws IOException {
906920
final CountDownLatch doneLatch = new CountDownLatch(1);
907921
try (ServerSocket socket = new MockServerSocket()) {
908922
socket.bind(getLocalEphemeral(), 1);
@@ -928,16 +942,56 @@ public void testTlsHandshakeTimeout() throws IOException {
928942
TransportRequestOptions.Type.REG,
929943
TransportRequestOptions.Type.STATE
930944
);
931-
final var future = new TestPlainActionFuture<Releasable>();
932-
serviceA.connectToNode(dummy, builder.build(), future);
933-
final var ex = expectThrows(ExecutionException.class, ConnectTransportException.class, future::get); // long wait
934-
assertEquals("[][" + dummy.getAddress() + "] connect_exception", ex.getMessage());
935-
assertNotNull(ExceptionsHelper.unwrap(ex, SslHandshakeTimeoutException.class));
945+
final ConnectTransportException exception;
946+
final var transportSettings = Settings.builder();
947+
if (handshakeTimeoutMillis == null) {
948+
handshakeTimeoutMillis = 10000L; // default
949+
} else {
950+
transportSettings.put("xpack.security.transport.ssl.handshake_timeout", TimeValue.timeValueMillis(handshakeTimeoutMillis));
951+
}
952+
try (var service = buildService(getTestName(), version0, transportVersion0, transportSettings.build())) {
953+
final var future = new TestPlainActionFuture<Releasable>();
954+
service.connectToNode(dummy, builder.build(), future);
955+
exception = expectThrows(ExecutionException.class, ConnectTransportException.class, future::get); // long wait
956+
assertEquals("[][" + dummy.getAddress() + "] connect_exception", exception.getMessage());
957+
assertThat(
958+
asInstanceOf(SSLHandshakeException.class, exception.getCause()).getMessage(),
959+
equalTo("handshake timed out after " + handshakeTimeoutMillis + "ms")
960+
);
961+
}
936962
} finally {
937963
doneLatch.countDown();
938964
}
939965
}
940966

967+
@SuppressForbidden(reason = "test needs a simple TCP connection")
968+
private void runInboundTlsHandshakeTimeoutTest(@Nullable /* to use the default */ Long handshakeTimeoutMillis) throws IOException {
969+
final var transportSettings = Settings.builder();
970+
if (handshakeTimeoutMillis == null) {
971+
handshakeTimeoutMillis = 10000L; // default
972+
} else {
973+
transportSettings.put("xpack.security.transport.ssl.handshake_timeout", TimeValue.timeValueMillis(handshakeTimeoutMillis));
974+
}
975+
try (
976+
var service = buildService(getTestName(), version0, transportVersion0, transportSettings.build());
977+
Socket clientSocket = new MockSocket();
978+
MockLog mockLog = MockLog.capture("org.elasticsearch.transport.netty4.ESLoggingHandler")
979+
) {
980+
mockLog.addExpectation(
981+
new MockLog.SeenEventExpectation(
982+
"timeout event message",
983+
"org.elasticsearch.transport.netty4.ESLoggingHandler",
984+
Level.TRACE,
985+
"SslHandshakeTimeoutException: handshake timed out after " + handshakeTimeoutMillis + "ms"
986+
)
987+
);
988+
989+
clientSocket.connect(service.boundAddress().boundAddresses()[0].address());
990+
expectThrows(EOFException.class, () -> clientSocket.getInputStream().skipNBytes(Long.MAX_VALUE));
991+
mockLog.assertAllExpectationsMatched();
992+
}
993+
}
994+
941995
public void testTcpHandshakeConnectionReset() throws IOException, InterruptedException {
942996
assumeFalse("Can't run in a FIPS JVM, TrustAllConfig is not a SunJSSE TrustManagers", inFipsJvm());
943997
SSLService sslService = createSSLService();

0 commit comments

Comments
 (0)