Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/130909.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 130909
summary: Allow adjustment of transport TLS handshake timeout
area: Network
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.elasticsearch.common.ssl.SslTrustConfig;
import org.elasticsearch.common.util.Maps;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.common.socket.SocketAccess;
Expand Down Expand Up @@ -118,9 +119,16 @@ public class SSLService {
Setting.Property.NodeScope
);

private static final Setting<TimeValue> TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING = Setting.positiveTimeSetting(
"xpack.security.transport.ssl.handshake_timeout",
TimeValue.timeValueSeconds(10),
Setting.Property.NodeScope
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, this will affect more than transport connections. It should at least also apply to RCS 2.0 remote cluster client and likely security realms that initiate outbound TLS connections, e.g. OIDC realm.

Most existing SSL settings are affix settings that apply to different contexts. The transport is one of the contexts. Defining these settings is a somewhat involved process via SSLConfigurationSettings to support contexts.

I think we should either:

  1. Support this new setting for different contexts as well.
  2. Dropping the transport part from the setting name, i.e. xpack.security.ssl.handshake_timeout, as well as updating the docs to indicate it applies more broadly.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it only affects transport connections, i.e. those which go via SecurityNetty4Transport. That does indeed include remote-cluster connections, but not other outbound TLS connections like the HTTPS ones involved in OIDC. I hadn't noticed that we count RCS2.0 transport connections as distinct from other transport connections in terms of this kind of configuration.

It's a bit tricky tho, I don't really want to have to add support for this setting to all the different contexts in which we do TLS handshakes. At least not today: progress over perfection and all that. If we called it xpack.security.ssl.handshake_timeout then that'd imply it worked everywhere. I'd rather keep it transport-specific, but I think I can see a way to add this to the RCS2.0 settings too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok see dc3a9ac

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you are right about this does not apply to realms.


private final Environment env;
private final Settings settings;
private final boolean diagnoseTrustExceptions;
private final long handshakeTimeoutMillis;

/**
* This is a mapping from "context name" (in general use, the name of a setting key)
Expand Down Expand Up @@ -156,6 +164,7 @@ public SSLService(Environment environment, Map<String, SslConfiguration> sslConf
this.env = environment;
this.settings = env.settings();
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings());
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(environment.settings()).millis();
this.sslConfigurations = sslConfigurations;
this.sslContexts = loadSslConfigurations(this.sslConfigurations);
}
Expand All @@ -166,6 +175,7 @@ public SSLService(Settings settings, Environment environment) {
this.env = environment;
this.settings = env.settings();
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(settings);
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(settings).millis();
this.sslConfigurations = getSSLConfigurations(env, this.settings);
this.sslContexts = loadSslConfigurations(this.sslConfigurations);
}
Expand All @@ -178,6 +188,7 @@ private SSLService(
this.env = environment;
this.settings = env.settings();
this.diagnoseTrustExceptions = DIAGNOSE_TRUST_EXCEPTIONS_SETTING.get(environment.settings());
this.handshakeTimeoutMillis = TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING.get(environment.settings()).millis();
this.sslConfigurations = sslConfigurations;
this.sslContexts = sslContexts;
}
Expand Down Expand Up @@ -214,6 +225,7 @@ SSLContextHolder sslContextHolder(SslConfiguration sslConfiguration) {

public static void registerSettings(List<Setting<?>> settingList) {
settingList.add(DIAGNOSE_TRUST_EXCEPTIONS_SETTING);
settingList.add(TRANSPORT_TLS_HANDSHAKE_TIMEOUT_SETTING);
}

/**
Expand Down Expand Up @@ -979,4 +991,8 @@ private static String sslContextAlgorithm(List<String> supportedProtocols) {
"no supported SSL/TLS protocol was found in the configured supported protocols: " + supportedProtocols
);
}

public long getHandshakeTimeoutMillis() {
return handshakeTimeoutMillis;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ protected void initChannel(Channel ch) throws Exception {
SSLEngine serverEngine = sslService.createSSLEngine(configuration, null, -1);
serverEngine.setUseClientMode(false);
final SslHandler sslHandler = new SslHandler(serverEngine);
sslHandler.setHandshakeTimeoutMillis(sslService.getHandshakeTimeoutMillis());
ch.pipeline().addFirst("sslhandler", sslHandler);
super.initChannel(ch);
assert ch.pipeline().first() == sslHandler : "SSL handler must be first handler in pipeline";
Expand Down Expand Up @@ -340,6 +341,7 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock
}
final ChannelPromise connectPromise = ctx.newPromise();
final SslHandler sslHandler = new SslHandler(sslEngine);
sslHandler.setHandshakeTimeoutMillis(sslService.getHandshakeTimeoutMillis());
ctx.pipeline().replace(this, "ssl", sslHandler);
final Future<?> handshakePromise = sslHandler.handshakeFuture();
Netty4Utils.addListener(connectPromise, result -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand All @@ -35,13 +36,17 @@
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;
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;
Expand All @@ -65,6 +70,7 @@
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;
Expand Down Expand Up @@ -902,7 +908,15 @@ public void testTcpHandshakeTimeout() throws IOException {
}
}

@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);
Expand All @@ -928,16 +942,56 @@ public void testTlsHandshakeTimeout() throws IOException {
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();
Expand Down
Loading