Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue deprecation warning if TLSv1.0 is used without explicit config #37788

Merged
merged 8 commits into from
Jan 29, 2019
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.network.CloseableChannel;
import org.elasticsearch.common.network.NetworkService;
Expand All @@ -31,17 +32,20 @@
import org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper;
import org.elasticsearch.xpack.core.ssl.SSLConfiguration;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.core.ssl.TLSv1DeprecationHandler;

import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import static org.elasticsearch.xpack.core.security.SecurityField.setting;

Expand All @@ -55,6 +59,7 @@ public class SecurityNetty4Transport extends Netty4Transport {
private final SSLConfiguration sslConfiguration;
private final Map<String, SSLConfiguration> profileConfiguration;
private final boolean sslEnabled;
private final Map<String, TLSv1DeprecationHandler> tlsDeprecation;

public SecurityNetty4Transport(
final Settings settings,
Expand All @@ -72,18 +77,42 @@ public SecurityNetty4Transport(
this.sslConfiguration = sslService.getSSLConfiguration(setting("transport.ssl."));
Map<String, SSLConfiguration> profileConfiguration = getTransportProfileConfigurations(settings, sslService, sslConfiguration);
this.profileConfiguration = Collections.unmodifiableMap(profileConfiguration);
this.tlsDeprecation = buildTlsDeprecationHandlers(settings, profileConfiguration.keySet());
} else {
this.profileConfiguration = Collections.emptyMap();
this.sslConfiguration = null;
this.tlsDeprecation = Collections.emptyMap();
}
}

// Package protected for testing
static Map<String, TLSv1DeprecationHandler> buildTlsDeprecationHandlers(Settings settings, Set<String> profileNames) {
TLSv1DeprecationHandler defaultTlsDeprecationHandler = new TLSv1DeprecationHandler(setting("transport.ssl."), settings, logger);
if (defaultTlsDeprecationHandler.shouldLogWarnings()) {
final Map<String, TLSv1DeprecationHandler> handlers = new HashMap<>();
profileNames.forEach(name -> {
if (TransportSettings.DEFAULT_PROFILE.equals(name)) {
handlers.put(name, defaultTlsDeprecationHandler);
} else {
handlers.put(name, new TLSv1DeprecationHandler(getTransportProfileSslPrefix(name) + ".", settings, logger));
}
});
return Collections.unmodifiableMap(handlers);
} else {
return Collections.emptyMap();
}
}

private static String getTransportProfileSslPrefix(String name) {
return "transport.profiles." + name + "." + setting("ssl") ;
}

public static Map<String, SSLConfiguration> getTransportProfileConfigurations(Settings settings, SSLService sslService,
SSLConfiguration defaultConfiguration) {
Set<String> profileNames = settings.getGroups("transport.profiles.", true).keySet();
Map<String, SSLConfiguration> profileConfiguration = new HashMap<>(profileNames.size() + 1);
for (String profileName : profileNames) {
SSLConfiguration configuration = sslService.getSSLConfiguration("transport.profiles." + profileName + "." + setting("ssl"));
SSLConfiguration configuration = sslService.getSSLConfiguration(getTransportProfileSslPrefix(profileName));
profileConfiguration.put(profileName, configuration);
}

Expand Down Expand Up @@ -155,9 +184,13 @@ public void onException(TcpChannel channel, Exception e) {
public class SslChannelInitializer extends ServerChannelInitializer {
private final SSLConfiguration configuration;

public SslChannelInitializer(String name, SSLConfiguration configuration) {
@Nullable
private final Consumer<SSLSession> handshakeListener;

public SslChannelInitializer(String name, SSLConfiguration configuration, Consumer<SSLSession> handshakeListener) {
super(name);
this.configuration = configuration;
this.handshakeListener = handshakeListener;
}

@Override
Expand All @@ -167,11 +200,30 @@ protected void initChannel(Channel ch) throws Exception {
serverEngine.setUseClientMode(false);
final SslHandler sslHandler = new SslHandler(serverEngine);
ch.pipeline().addFirst("sslhandler", sslHandler);
if (handshakeListener != null) {
sslHandler.handshakeFuture().addListener(fut -> {
SSLSession session = serverEngine.getSession();
handshakeListener.accept(session);
});
}
}
}

protected ServerChannelInitializer getSslChannelInitializer(final String name, final SSLConfiguration configuration) {
return new SslChannelInitializer(name, sslConfiguration);
return new SslChannelInitializer(name, sslConfiguration, getSslHandshakeListenerForProfile(name));
}

@Nullable
protected Consumer<SSLSession> getSslHandshakeListenerForProfile(String name) {
// This is handled here (rather than as an interceptor) so we know which profile was used, and can
// provide the correct setting to report on (this may be technically also possible in a transport interceptor, but the
// existing security interceptor doesn't have that now, and adding it would be a more intrusive change).
final TLSv1DeprecationHandler deprecationHandler = tlsDeprecation.get(name);
if (deprecationHandler != null && deprecationHandler.shouldLogWarnings()) {
return session -> deprecationHandler.checkAndLog(session, () -> "transport profile: " + name);
} else {
return null;
}
}

private class SecurityClientChannelInitializer extends ClientChannelInitializer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.elasticsearch.xpack.core.XPackSettings;

import javax.net.ssl.TrustManagerFactory;

import java.security.KeyStore;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -62,7 +61,7 @@ public class SSLConfigurationSettings {
public static final Setting<List<String>> CIPHERS_SETTING_PROFILES = Setting.affixKeySetting("transport.profiles.",
"xpack.security.ssl.cipher_suites", CIPHERS_SETTING_TEMPLATE);

private static final Function<String,Setting<List<String>>> SUPPORTED_PROTOCOLS_TEMPLATE = key -> Setting.listSetting(key,
static final Function<String, Setting<List<String>>> SUPPORTED_PROTOCOLS_TEMPLATE = key -> Setting.listSetting(key,
Collections.emptyList(), Function.identity(), propertiesFromKey(key));
public static final Setting<List<String>> SUPPORTED_PROTOCOLS_PROFILES = Setting.affixKeySetting("transport.profiles.",
"xpack.security.ssl.supported_protocols", SUPPORTED_PROTOCOLS_TEMPLATE) ;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,20 @@ SSLContextHolder sslContextHolder(SSLConfiguration sslConfiguration) {
*
* @param settings the settings used to identify the ssl configuration, typically under a *.ssl. prefix. An empty settings will return
* a context created from the default configuration
* @param tlsDeprecationHandler a handler in case TLSv1.0 is used for an SSL connection under this strategy
* @return Never {@code null}.
* @deprecated This method will fail if the SSL configuration uses a {@link org.elasticsearch.common.settings.SecureSetting} but the
* {@link org.elasticsearch.common.settings.SecureSettings} have been closed. Use {@link #getSSLConfiguration(String)}
* and {@link #sslIOSessionStrategy(SSLConfiguration)} (Deprecated, but not removed because monitoring uses dynamic SSL settings)
* and {@link #sslIOSessionStrategy(SSLConfiguration, TLSv1DeprecationHandler)}
* (Deprecated, but not removed because monitoring uses dynamic SSL settings)
*/
@Deprecated
public SSLIOSessionStrategy sslIOSessionStrategy(Settings settings) {
public SSLIOSessionStrategy sslIOSessionStrategy(Settings settings, TLSv1DeprecationHandler tlsDeprecationHandler) {
SSLConfiguration config = sslConfiguration(settings);
return sslIOSessionStrategy(config);
return sslIOSessionStrategy(config, tlsDeprecationHandler);
}

public SSLIOSessionStrategy sslIOSessionStrategy(SSLConfiguration config) {
public SSLIOSessionStrategy sslIOSessionStrategy(SSLConfiguration config, TLSv1DeprecationHandler tlsDeprecationHandler) {
SSLContext sslContext = sslContext(config);
String[] ciphers = supportedCiphers(sslParameters(sslContext).getCipherSuites(), config.cipherSuites(), false);
String[] supportedProtocols = config.supportedProtocols().toArray(Strings.EMPTY_ARRAY);
Expand All @@ -177,10 +179,24 @@ public SSLIOSessionStrategy sslIOSessionStrategy(SSLConfiguration config) {
} else {
verifier = NoopHostnameVerifier.INSTANCE;
}
verifier = wrapHostnameVerifier(verifier, tlsDeprecationHandler);

return sslIOSessionStrategy(sslContext, supportedProtocols, ciphers, verifier);
}

public HostnameVerifier wrapHostnameVerifier(final HostnameVerifier verifier, TLSv1DeprecationHandler tlsDeprecationHandler) {
// Using the hostname verifier for this is just ugly, but HTTP client doesn't expose the SSLSession in many places
// and this is the easiest one to hook into in a non-intrusive way
if (tlsDeprecationHandler.shouldLogWarnings()) {
return (hostname, session) -> {
tlsDeprecationHandler.checkAndLog(session, () -> "http connection to " + hostname);
return verifier.verify(hostname, session);
};
} else {
return verifier;
}
}

/**
* The {@link SSLParameters} that are associated with the {@code sslContext}.
* <p>
Expand All @@ -194,8 +210,8 @@ SSLParameters sslParameters(SSLContext sslContext) {
}

/**
* This method only exists to simplify testing of {@link #sslIOSessionStrategy(Settings)} because {@link SSLIOSessionStrategy} does
* not expose any of the parameters that you give it.
* This method only exists to simplify testing of {@link #sslIOSessionStrategy(Settings, TLSv1DeprecationHandler)} because
* {@link SSLIOSessionStrategy} does not expose any of the parameters that you give it.
*
* @param sslContext SSL Context used to handle SSL / TCP requests
* @param protocols Supported protocols
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.core.ssl;

import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Settings;

import javax.net.ssl.SSLSession;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.function.Supplier;

import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.SUPPORTED_PROTOCOLS_TEMPLATE;

/**
* Handles logging deprecation warnings when a TLSv1.0 SSL connection is used, and that SSL context relies on
* the default list of supported_protocols (in Elasticsearch 7.0, this list will not include TLS 1.0).
*/
public class TLSv1DeprecationHandler {

private final String supportedProtocolsSetting;
private final boolean shouldLogWarnings;
private final DeprecationLogger deprecationLogger;

public TLSv1DeprecationHandler(String settingPrefix, Settings settings, Logger baseLogger) {
if (settingPrefix.length() > 0 && settingPrefix.endsWith("ssl.") == false) {
throw new IllegalArgumentException("Setting prefix [" + settingPrefix + "] must end in 'ssl.'");
}
this.supportedProtocolsSetting = settingPrefix + "supported_protocols";
this.shouldLogWarnings = SUPPORTED_PROTOCOLS_TEMPLATE.apply(supportedProtocolsSetting).exists(settings) == false;
if (shouldLogWarnings) {
deprecationLogger = new DeprecationLogger(baseLogger);
} else {
deprecationLogger = null;
}
}

private TLSv1DeprecationHandler(String settingKey, boolean shouldLog, DeprecationLogger logger) {
this.supportedProtocolsSetting = settingKey;
this.shouldLogWarnings = shouldLog;
this.deprecationLogger = logger;
}

public static TLSv1DeprecationHandler disabled() {
return new TLSv1DeprecationHandler(null, false, null);
}

public boolean shouldLogWarnings() {
return shouldLogWarnings;
}

public void checkAndLog(SSLSession session, Supplier<String> descriptionSupplier) {
if (shouldLogWarnings == false) {
return;
}
if ("TLSv1".equals(session.getProtocol())) {
final String description = descriptionSupplier.get();
// Use a "LRU" key that is unique per day. That way each description (source address, etc) will be logged once per day.
final String key = LocalDate.now(ZoneId.of("UTC")) + ":" + description;
deprecationLogger.deprecatedAndMaybeLog(key,
"a TLS v1.0 session was used for [{}], " +
"this protocol will be disabled by default in a future version. " +
"The [{}] setting can be used to control this.",
description, supportedProtocolsSetting);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.common.Strings;
Expand All @@ -20,6 +20,7 @@
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.mocksocket.MockHttpServer;
import org.elasticsearch.xpack.core.XPackSettings;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
Expand Down Expand Up @@ -55,6 +56,7 @@ public class MockWebServer implements Closeable {
private final Logger logger;
private final SSLContext sslContext;
private final boolean needClientAuth;
private final List<String> supportedProtocols;
private final Set<CountDownLatch> latches = ConcurrentCollections.newConcurrentSet();
private String hostname;
private int port;
Expand All @@ -72,9 +74,20 @@ public MockWebServer() {
* @param needClientAuth Should clientAuth be used, which requires a client side certificate
*/
public MockWebServer(SSLContext sslContext, boolean needClientAuth) {
this(sslContext, needClientAuth, XPackSettings.DEFAULT_SUPPORTED_PROTOCOLS);
}

/**
* Instantiates a webserver with https
* @param sslContext The SSL context to be used for encryption
* @param needClientAuth Should clientAuth be used, which requires a client side certificate
* @param supportedProtocols Which SSL/TLS protocols version should be supported
*/
public MockWebServer(SSLContext sslContext, boolean needClientAuth, List<String> supportedProtocols) {
this.needClientAuth = needClientAuth;
this.logger = LogManager.getLogger(this.getClass());
this.sslContext = sslContext;
this.supportedProtocols = supportedProtocols;
}

/**
Expand All @@ -87,7 +100,7 @@ public void start() throws IOException {
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0);
if (sslContext != null) {
HttpsServer httpsServer = MockHttpServer.createHttps(address, 0);
httpsServer.setHttpsConfigurator(new CustomHttpsConfigurator(sslContext, needClientAuth));
httpsServer.setHttpsConfigurator(new CustomHttpsConfigurator(sslContext, needClientAuth, supportedProtocols));
server = httpsServer;
} else {
server = MockHttpServer.createHttp(address, 0);
Expand Down Expand Up @@ -144,15 +157,18 @@ public void start() throws IOException {
private static final class CustomHttpsConfigurator extends HttpsConfigurator {

private final boolean needClientAuth;
private final List<String> supportedProtocols;

CustomHttpsConfigurator(SSLContext sslContext, boolean needClientAuth) {
CustomHttpsConfigurator(SSLContext sslContext, boolean needClientAuth, List<String> supportedProtocols) {
super(sslContext);
this.needClientAuth = needClientAuth;
this.supportedProtocols = supportedProtocols;
}

@Override
public void configure(HttpsParameters params) {
params.setNeedClientAuth(needClientAuth);
params.setProtocols(this.supportedProtocols.toArray(Strings.EMPTY_ARRAY));
}
}

Expand Down
Loading