diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java index 78a1425500072..51821c73329ca 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java @@ -112,7 +112,7 @@ public void sendMessage(BytesReference reference, ActionListener listener) } } - public Channel getLowLevelChannel() { + public Channel getNettyChannel() { return channel; } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java index ad81719ebcbb9..3dcd59cf8e28c 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java @@ -51,8 +51,8 @@ public class HttpReadWriteHandler implements ReadWriteHandler { private final NioHttpChannel nioHttpChannel; private final NioHttpServerTransport transport; - HttpReadWriteHandler(NioHttpChannel nioHttpChannel, NioHttpServerTransport transport, HttpHandlingSettings settings, - NioCorsConfig corsConfig) { + public HttpReadWriteHandler(NioHttpChannel nioHttpChannel, NioHttpServerTransport transport, HttpHandlingSettings settings, + NioCorsConfig corsConfig) { this.nioHttpChannel = nioHttpChannel; this.transport = transport; diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpChannel.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpChannel.java index 0a797a5687ec7..1a4c5f14c91da 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpChannel.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpChannel.java @@ -28,7 +28,7 @@ public class NioHttpChannel extends NioSocketChannel implements HttpChannel { - NioHttpChannel(SocketChannel socketChannel) { + public NioHttpChannel(SocketChannel socketChannel) { super(socketChannel); } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerChannel.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerChannel.java index 2674d38dc490e..d72376da5c03b 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerChannel.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerChannel.java @@ -23,12 +23,11 @@ import org.elasticsearch.http.HttpServerChannel; import org.elasticsearch.nio.NioServerSocketChannel; -import java.io.IOException; import java.nio.channels.ServerSocketChannel; public class NioHttpServerChannel extends NioServerSocketChannel implements HttpServerChannel { - NioHttpServerChannel(ServerSocketChannel serverSocketChannel) throws IOException { + public NioHttpServerChannel(ServerSocketChannel serverSocketChannel) { super(serverSocketChannel); } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java index b80778e964293..9c672c1caf15a 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java @@ -35,7 +35,6 @@ import org.elasticsearch.http.AbstractHttpServerTransport; import org.elasticsearch.http.HttpChannel; import org.elasticsearch.http.HttpServerChannel; -import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.http.nio.cors.NioCorsConfig; import org.elasticsearch.http.nio.cors.NioCorsConfigBuilder; import org.elasticsearch.nio.BytesChannelContext; @@ -87,21 +86,21 @@ public class NioHttpServerTransport extends AbstractHttpServerTransport { (s) -> Integer.toString(EsExecutors.numberOfProcessors(s) * 2), (s) -> Setting.parseInt(s, 1, "http.nio.worker_count"), Setting.Property.NodeScope); - private final PageCacheRecycler pageCacheRecycler; + protected final PageCacheRecycler pageCacheRecycler; + protected final NioCorsConfig corsConfig; - private final boolean tcpNoDelay; - private final boolean tcpKeepAlive; - private final boolean reuseAddress; - private final int tcpSendBufferSize; - private final int tcpReceiveBufferSize; + protected final boolean tcpNoDelay; + protected final boolean tcpKeepAlive; + protected final boolean reuseAddress; + protected final int tcpSendBufferSize; + protected final int tcpReceiveBufferSize; private NioGroup nioGroup; - private HttpChannelFactory channelFactory; - private final NioCorsConfig corsConfig; + private ChannelFactory channelFactory; public NioHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, - HttpServerTransport.Dispatcher dispatcher) { + Dispatcher dispatcher) { super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher); this.pageCacheRecycler = pageCacheRecycler; @@ -136,7 +135,7 @@ protected void doStart() { nioGroup = new NioGroup(daemonThreadFactory(this.settings, HTTP_SERVER_ACCEPTOR_THREAD_NAME_PREFIX), acceptorCount, daemonThreadFactory(this.settings, HTTP_SERVER_WORKER_THREAD_NAME_PREFIX), workerCount, (s) -> new EventHandler(this::onNonChannelException, s)); - channelFactory = new HttpChannelFactory(); + channelFactory = channelFactory(); bindServer(); success = true; } catch (IOException e) { @@ -162,6 +161,10 @@ protected HttpServerChannel bind(InetSocketAddress socketAddress) throws IOExcep return nioGroup.bindServerChannel(socketAddress, channelFactory); } + protected ChannelFactory channelFactory() { + return new HttpChannelFactory(); + } + static NioCorsConfig buildCorsConfig(Settings settings) { if (SETTING_CORS_ENABLED.get(settings) == false) { return NioCorsConfigBuilder.forOrigins().disable().build(); @@ -194,7 +197,7 @@ static NioCorsConfig buildCorsConfig(Settings settings) { .build(); } - private void acceptChannel(NioSocketChannel socketChannel) { + protected void acceptChannel(NioSocketChannel socketChannel) { super.serverAcceptedChannel((HttpChannel) socketChannel); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 26ec50c0eb3c4..3115c08a9469d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -200,11 +200,13 @@ import org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction; import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.netty4.SecurityNetty4HttpServerTransport; import org.elasticsearch.xpack.security.transport.netty4.SecurityNetty4ServerTransport; import org.elasticsearch.xpack.core.template.TemplateUtils; +import org.elasticsearch.xpack.security.transport.nio.SecurityNioHttpServerTransport; import org.elasticsearch.xpack.security.transport.nio.SecurityNioTransport; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -511,21 +513,22 @@ static Settings additionalSettings(final Settings settings, final boolean enable if (NetworkModule.HTTP_TYPE_SETTING.exists(settings)) { final String httpType = NetworkModule.HTTP_TYPE_SETTING.get(settings); - if (httpType.equals(SecurityField.NAME4)) { - SecurityNetty4HttpServerTransport.overrideSettings(builder, settings); + if (httpType.equals(SecurityField.NAME4) || httpType.equals(SecurityField.NIO)) { + SecurityHttpSettings.overrideSettings(builder, settings); } else { final String message = String.format( Locale.ROOT, - "http type setting [%s] must be [%s] but is [%s]", + "http type setting [%s] must be [%s] or [%s] but is [%s]", NetworkModule.HTTP_TYPE_KEY, SecurityField.NAME4, + SecurityField.NIO, httpType); throw new IllegalArgumentException(message); } } else { // default to security4 builder.put(NetworkModule.HTTP_TYPE_KEY, SecurityField.NAME4); - SecurityNetty4HttpServerTransport.overrideSettings(builder, settings); + SecurityHttpSettings.overrideSettings(builder, settings); } builder.put(SecuritySettings.addUserSettings(settings)); return builder.build(); @@ -869,8 +872,14 @@ public Map> getHttpTransports(Settings set if (enabled == false) { // don't register anything if we are not enabled return Collections.emptyMap(); } - return Collections.singletonMap(SecurityField.NAME4, () -> new SecurityNetty4HttpServerTransport(settings, - networkService, bigArrays, ipFilter.get(), getSslService(), threadPool, xContentRegistry, dispatcher)); + + Map> httpTransports = new HashMap<>(); + httpTransports.put(SecurityField.NAME4, () -> new SecurityNetty4HttpServerTransport(settings, networkService, bigArrays, + ipFilter.get(), getSslService(), threadPool, xContentRegistry, dispatcher)); + httpTransports.put(SecurityField.NIO, () -> new SecurityNioHttpServerTransport(settings, networkService, bigArrays, + pageCacheRecycler, threadPool, xContentRegistry, dispatcher, ipFilter.get(), getSslService())); + + return httpTransports; } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java index 9109bb37e8c41..8d304302e03ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.security.rest; -import io.netty.channel.Channel; -import io.netty.handler.ssl.SslHandler; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; @@ -15,7 +13,6 @@ import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.http.HttpChannel; -import org.elasticsearch.http.netty4.Netty4HttpChannel; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestChannel; @@ -24,7 +21,7 @@ import org.elasticsearch.rest.RestRequest.Method; import org.elasticsearch.xpack.core.security.rest.RestRequestFilter; import org.elasticsearch.xpack.security.authc.AuthenticationService; -import org.elasticsearch.xpack.security.transport.ServerTransportFilter; +import org.elasticsearch.xpack.security.transport.SSLEngineUtils; import java.io.IOException; @@ -53,10 +50,7 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c // CORS - allow for preflight unauthenticated OPTIONS request if (extractClientCertificate) { HttpChannel httpChannel = request.getHttpChannel(); - Channel nettyChannel = ((Netty4HttpChannel) httpChannel).getNettyChannel(); - SslHandler handler = nettyChannel.pipeline().get(SslHandler.class); - assert handler != null; - ServerTransportFilter.extractClientCertificates(logger, threadContext, handler.engine(), nettyChannel); + SSLEngineUtils.extractClientCertificates(logger, threadContext, httpChannel); } service.authenticate(maybeWrapRestRequest(request), ActionListener.wrap( authentication -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SSLEngineUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SSLEngineUtils.java new file mode 100644 index 0000000000000..5bbcbaa050917 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SSLEngineUtils.java @@ -0,0 +1,93 @@ +/* + * 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.security.transport; + +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.Supplier; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.http.HttpChannel; +import org.elasticsearch.http.netty4.Netty4HttpChannel; +import org.elasticsearch.http.nio.NioHttpChannel; +import org.elasticsearch.nio.SocketChannelContext; +import org.elasticsearch.transport.TcpChannel; +import org.elasticsearch.transport.netty4.Netty4TcpChannel; +import org.elasticsearch.transport.nio.NioTcpChannel; +import org.elasticsearch.xpack.security.authc.pki.PkiRealm; +import org.elasticsearch.xpack.security.transport.nio.SSLChannelContext; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +public class SSLEngineUtils { + + private SSLEngineUtils() {} + + public static void extractClientCertificates(Logger logger, ThreadContext threadContext, HttpChannel httpChannel) { + SSLEngine sslEngine = getSSLEngine(httpChannel); + extract(logger, threadContext, sslEngine, httpChannel); + } + + public static void extractClientCertificates(Logger logger, ThreadContext threadContext, TcpChannel tcpChannel) { + SSLEngine sslEngine = getSSLEngine(tcpChannel); + extract(logger, threadContext, sslEngine, tcpChannel); + } + + public static SSLEngine getSSLEngine(HttpChannel httpChannel) { + if (httpChannel instanceof Netty4HttpChannel) { + Channel nettyChannel = ((Netty4HttpChannel) httpChannel).getNettyChannel(); + SslHandler handler = nettyChannel.pipeline().get(SslHandler.class); + assert handler != null : "Must have SslHandler"; + return handler.engine(); + } else if (httpChannel instanceof NioHttpChannel) { + SocketChannelContext context = ((NioHttpChannel) httpChannel).getContext(); + assert context instanceof SSLChannelContext : "Must be SSLChannelContext.class, found: " + context.getClass(); + return ((SSLChannelContext) context).getSSLEngine(); + } else { + throw new AssertionError("Unknown channel class type: " + httpChannel.getClass()); + } + } + + public static SSLEngine getSSLEngine(TcpChannel tcpChannel) { + if (tcpChannel instanceof Netty4TcpChannel) { + Channel nettyChannel = ((Netty4TcpChannel) tcpChannel).getNettyChannel(); + SslHandler handler = nettyChannel.pipeline().get(SslHandler.class); + assert handler != null : "Must have SslHandler"; + return handler.engine(); + } else if (tcpChannel instanceof NioTcpChannel) { + SocketChannelContext context = ((NioTcpChannel) tcpChannel).getContext(); + assert context instanceof SSLChannelContext : "Must be SSLChannelContext.class, found: " + context.getClass(); + return ((SSLChannelContext) context).getSSLEngine(); + } else { + throw new AssertionError("Unknown channel class type: " + tcpChannel.getClass()); + } + } + + private static void extract(Logger logger, ThreadContext threadContext, SSLEngine sslEngine, Object channel) { + try { + Certificate[] certs = sslEngine.getSession().getPeerCertificates(); + if (certs instanceof X509Certificate[]) { + threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, certs); + } + } catch (SSLPeerUnverifiedException e) { + // this happens when client authentication is optional and the client does not provide credentials. If client + // authentication was required then this connection should be closed before ever getting into this class + assert sslEngine.getNeedClientAuth() == false; + assert sslEngine.getWantClientAuth(); + if (logger.isTraceEnabled()) { + logger.trace( + (Supplier) () -> new ParameterizedMessage( + "SSL Peer did not present a certificate on channel [{}]", channel), e); + } else if (logger.isDebugEnabled()) { + logger.debug("SSL Peer did not present a certificate on channel [{}]", channel); + } + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java new file mode 100644 index 0000000000000..c1999c5ddfba2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java @@ -0,0 +1,64 @@ +/* + * 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.security.transport; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.network.CloseableChannel; +import org.elasticsearch.http.HttpChannel; + +import java.util.function.BiConsumer; + +import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isCloseDuringHandshakeException; +import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isNotSslRecordException; +import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isReceivedCertificateUnknownException; + +public final class SecurityHttpExceptionHandler implements BiConsumer { + + private final Lifecycle lifecycle; + private final Logger logger; + private final BiConsumer fallback; + + public SecurityHttpExceptionHandler(Logger logger, Lifecycle lifecycle, BiConsumer fallback) { + this.lifecycle = lifecycle; + this.logger = logger; + this.fallback = fallback; + } + + public void accept(HttpChannel channel, Exception e) { + if (!lifecycle.started()) { + return; + } + + if (isNotSslRecordException(e)) { + if (logger.isTraceEnabled()) { + logger.trace(new ParameterizedMessage("received plaintext http traffic on a https channel, closing connection {}", + channel), e); + } else { + logger.warn("received plaintext http traffic on a https channel, closing connection {}", channel); + } + CloseableChannel.closeChannel(channel); + } else if (isCloseDuringHandshakeException(e)) { + if (logger.isTraceEnabled()) { + logger.trace(new ParameterizedMessage("connection {} closed during ssl handshake", channel), e); + } else { + logger.warn("connection {} closed during ssl handshake", channel); + } + CloseableChannel.closeChannel(channel); + } else if (isReceivedCertificateUnknownException(e)) { + if (logger.isTraceEnabled()) { + logger.trace(new ParameterizedMessage("http client did not trust server's certificate, closing connection {}", + channel), e); + } else { + logger.warn("http client did not trust this server's certificate, closing connection {}", channel); + } + CloseableChannel.closeChannel(channel); + } else { + fallback.accept(channel, e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettings.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettings.java new file mode 100644 index 0000000000000..f8079535acf99 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettings.java @@ -0,0 +1,22 @@ +/* + * 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.security.transport; + +import org.elasticsearch.common.settings.Settings; + +import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_COMPRESSION; +import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; + +public final class SecurityHttpSettings { + + private SecurityHttpSettings() {} + + public static void overrideSettings(Settings.Builder settingsBuilder, Settings settings) { + if (HTTP_SSL_ENABLED.get(settings) && SETTING_HTTP_COMPRESSION.exists(settings) == false) { + settingsBuilder.put(SETTING_HTTP_COMPRESSION.getKey(), false); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index 9427812ba1349..2f0c40c1fdd16 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -5,11 +5,7 @@ */ package org.elasticsearch.xpack.security.transport; -import io.netty.channel.Channel; -import io.netty.handler.ssl.SslHandler; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.IndicesRequest; @@ -20,11 +16,13 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.transport.TaskTransportChannel; +import org.elasticsearch.transport.TcpChannel; import org.elasticsearch.transport.TcpTransportChannel; import org.elasticsearch.transport.TransportChannel; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportService; import org.elasticsearch.transport.netty4.Netty4TcpChannel; +import org.elasticsearch.transport.nio.NioTcpChannel; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.user.KibanaUser; @@ -32,16 +30,10 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.action.SecurityActionMapper; import org.elasticsearch.xpack.security.authc.AuthenticationService; -import org.elasticsearch.xpack.security.authc.pki.PkiRealm; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationUtils; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLPeerUnverifiedException; - import java.io.IOException; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; @@ -115,13 +107,12 @@ requests from all the nodes are attached with a user (either a serialize unwrappedChannel = ((TaskTransportChannel) unwrappedChannel).getChannel(); } - if (extractClientCert && (unwrappedChannel instanceof TcpTransportChannel) && - ((TcpTransportChannel) unwrappedChannel).getChannel() instanceof Netty4TcpChannel) { - Channel channel = ((Netty4TcpChannel) ((TcpTransportChannel) unwrappedChannel).getChannel()).getLowLevelChannel(); - SslHandler sslHandler = channel.pipeline().get(SslHandler.class); - if (channel.isOpen()) { - assert sslHandler != null : "channel [" + channel + "] did not have a ssl handler. pipeline " + channel.pipeline(); - extractClientCertificates(logger, threadContext, sslHandler.engine(), channel); + if (extractClientCert && (unwrappedChannel instanceof TcpTransportChannel)) { + TcpChannel tcpChannel = ((TcpTransportChannel) unwrappedChannel).getChannel(); + if (tcpChannel instanceof Netty4TcpChannel || tcpChannel instanceof NioTcpChannel) { + if (tcpChannel.isOpen()) { + SSLEngineUtils.extractClientCertificates(logger, threadContext, tcpChannel); + } } } @@ -172,27 +163,6 @@ private void executeAsCurrentVersionKibanaUser(String securityAction, TransportR } } - static void extractClientCertificates(Logger logger, ThreadContext threadContext, SSLEngine sslEngine, Channel channel) { - try { - Certificate[] certs = sslEngine.getSession().getPeerCertificates(); - if (certs instanceof X509Certificate[]) { - threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, certs); - } - } catch (SSLPeerUnverifiedException e) { - // this happens when client authentication is optional and the client does not provide credentials. If client - // authentication was required then this connection should be closed before ever getting into this class - assert sslEngine.getNeedClientAuth() == false; - assert sslEngine.getWantClientAuth(); - if (logger.isTraceEnabled()) { - logger.trace( - (Supplier) () -> new ParameterizedMessage( - "SSL Peer did not present a certificate on channel [{}]", channel), e); - } else if (logger.isDebugEnabled()) { - logger.debug("SSL Peer did not present a certificate on channel [{}]", channel); - } - } - } - /** * A server transport filter rejects internal calls, which should be used on connections * where only clients connect to. This ensures that no client can send any internal actions diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java index d7a609f6f14ba..a728467f8bde7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java @@ -8,8 +8,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.handler.ssl.SslHandler; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; @@ -19,18 +17,16 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.transport.SecurityHttpExceptionHandler; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import javax.net.ssl.SSLEngine; -import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_COMPRESSION; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isCloseDuringHandshakeException; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isNotSslRecordException; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isReceivedCertificateUnknownException; public class SecurityNetty4HttpServerTransport extends Netty4HttpServerTransport { + private final SecurityHttpExceptionHandler securityExceptionHandler; private final IPFilter ipFilter; private final SSLService sslService; private final SSLConfiguration sslConfiguration; @@ -39,6 +35,7 @@ public SecurityNetty4HttpServerTransport(Settings settings, NetworkService netwo SSLService sslService, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, Dispatcher dispatcher) { super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher); + this.securityExceptionHandler = new SecurityHttpExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); this.ipFilter = ipFilter; final boolean ssl = HTTP_SSL_ENABLED.get(settings); this.sslService = sslService; @@ -51,41 +48,11 @@ public SecurityNetty4HttpServerTransport(Settings settings, NetworkService netwo } else { this.sslConfiguration = null; } - } @Override protected void onException(HttpChannel channel, Exception e) { - if (!lifecycle.started()) { - return; - } - - if (isNotSslRecordException(e)) { - if (logger.isTraceEnabled()) { - logger.trace(new ParameterizedMessage("received plaintext http traffic on a https channel, closing connection {}", - channel), e); - } else { - logger.warn("received plaintext http traffic on a https channel, closing connection {}", channel); - } - CloseableChannel.closeChannel(channel); - } else if (isCloseDuringHandshakeException(e)) { - if (logger.isTraceEnabled()) { - logger.trace(new ParameterizedMessage("connection {} closed during ssl handshake", channel), e); - } else { - logger.warn("connection {} closed during ssl handshake", channel); - } - CloseableChannel.closeChannel(channel); - } else if (isReceivedCertificateUnknownException(e)) { - if (logger.isTraceEnabled()) { - logger.trace(new ParameterizedMessage("http client did not trust server's certificate, closing connection {}", - channel), e); - } else { - logger.warn("http client did not trust this server's certificate, closing connection {}", channel); - } - CloseableChannel.closeChannel(channel); - } else { - super.onException(channel, e); - } + securityExceptionHandler.accept(channel, e); } @Override @@ -115,10 +82,4 @@ protected void initChannel(Channel ch) throws Exception { ch.pipeline().addFirst("ip_filter", new IpFilterRemoteAddressFilter(ipFilter, IPFilter.HTTP_PROFILE_NAME)); } } - - public static void overrideSettings(Settings.Builder settingsBuilder, Settings settings) { - if (HTTP_SSL_ENABLED.get(settings) && SETTING_HTTP_COMPRESSION.exists(settings) == false) { - settingsBuilder.put(SETTING_HTTP_COMPRESSION.getKey(), false); - } - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilter.java new file mode 100644 index 0000000000000..afb13ceff2edd --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilter.java @@ -0,0 +1,32 @@ +/* + * 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.security.transport.nio; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.nio.NioSocketChannel; +import org.elasticsearch.xpack.security.transport.filter.IPFilter; + +import java.util.function.Predicate; + +public final class NioIPFilter implements Predicate { + + private final IPFilter filter; + private final String profile; + + NioIPFilter(@Nullable IPFilter filter, String profile) { + this.filter = filter; + this.profile = profile; + } + + @Override + public boolean test(NioSocketChannel nioChannel) { + if (filter != null) { + return filter.accept(profile, nioChannel.getRemoteAddress()); + } else { + return true; + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLChannelContext.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLChannelContext.java index da348ea1f78e1..c83bd16ca95e1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLChannelContext.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLChannelContext.java @@ -14,6 +14,7 @@ import org.elasticsearch.nio.NioSelector; import org.elasticsearch.nio.WriteOperation; +import javax.net.ssl.SSLEngine; import java.io.IOException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -164,6 +165,10 @@ public void closeFromSelector() throws IOException { } } + public SSLEngine getSSLEngine() { + return sslDriver.getSSLEngine(); + } + private static class CloseNotifyOperation implements WriteOperation { private static final BiConsumer LISTENER = (v, t) -> {}; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLDriver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLDriver.java index 4080574713cce..382230684c77f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLDriver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SSLDriver.java @@ -96,6 +96,10 @@ public void renegotiate() throws SSLException { } } + public SSLEngine getSSLEngine() { + return engine; + } + public boolean hasFlushPending() { return networkWriteBuffer.hasRemaining(); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java new file mode 100644 index 0000000000000..006c78b4ae0de --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java @@ -0,0 +1,132 @@ +/* + * 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.security.transport.nio; + +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.recycler.Recycler; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.http.nio.HttpReadWriteHandler; +import org.elasticsearch.http.nio.NioHttpChannel; +import org.elasticsearch.http.nio.NioHttpServerChannel; +import org.elasticsearch.http.nio.NioHttpServerTransport; +import org.elasticsearch.nio.BytesChannelContext; +import org.elasticsearch.nio.ChannelFactory; +import org.elasticsearch.nio.InboundChannelBuffer; +import org.elasticsearch.nio.NioSelector; +import org.elasticsearch.nio.NioSocketChannel; +import org.elasticsearch.nio.ServerChannelContext; +import org.elasticsearch.nio.SocketChannelContext; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ssl.SSLConfiguration; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.transport.SecurityHttpExceptionHandler; +import org.elasticsearch.xpack.security.transport.filter.IPFilter; + +import javax.net.ssl.SSLEngine; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; + +public class SecurityNioHttpServerTransport extends NioHttpServerTransport { + + private final SecurityHttpExceptionHandler securityExceptionHandler; + private final IPFilter ipFilter; + private final NioIPFilter nioIpFilter; + private final SSLService sslService; + private final SSLConfiguration sslConfiguration; + private final boolean sslEnabled; + + public SecurityNioHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, ThreadPool threadPool, + NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, IPFilter ipFilter, + SSLService sslService) { + super(settings, networkService, bigArrays, pageCacheRecycler, threadPool, xContentRegistry, dispatcher); + this.securityExceptionHandler = new SecurityHttpExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); + this.ipFilter = ipFilter; + this.nioIpFilter = new NioIPFilter(ipFilter, IPFilter.HTTP_PROFILE_NAME); + this.sslEnabled = HTTP_SSL_ENABLED.get(settings); + this.sslService = sslService; + if (sslEnabled) { + this.sslConfiguration = sslService.sslConfiguration(SSLService.getHttpTransportSSLSettings(settings), Settings.EMPTY); + if (sslService.isConfigurationValidForServerUsage(sslConfiguration) == false) { + throw new IllegalArgumentException("a key must be provided to run as a server. the key should be configured using the " + + "[xpack.security.http.ssl.key] or [xpack.security.http.ssl.keystore.path] setting"); + } + } else { + this.sslConfiguration = null; + } + } + + @Override + protected void doStart() { + super.doStart(); + ipFilter.setBoundHttpTransportAddress(this.boundAddress()); + } + + protected SecurityHttpChannelFactory channelFactory() { + return new SecurityHttpChannelFactory(); + } + + class SecurityHttpChannelFactory extends ChannelFactory { + + private SecurityHttpChannelFactory() { + super(new RawChannelFactory(tcpNoDelay, tcpKeepAlive, reuseAddress, tcpSendBufferSize, tcpReceiveBufferSize)); + } + + @Override + public NioHttpChannel createChannel(NioSelector selector, SocketChannel channel) throws IOException { + NioHttpChannel httpChannel = new NioHttpChannel(channel); + Supplier pageSupplier = () -> { + Recycler.V bytes = pageCacheRecycler.bytePage(false); + return new InboundChannelBuffer.Page(ByteBuffer.wrap(bytes.v()), bytes::close); + }; + HttpReadWriteHandler httpHandler = new HttpReadWriteHandler(httpChannel,SecurityNioHttpServerTransport.this, + handlingSettings, corsConfig); + InboundChannelBuffer buffer = new InboundChannelBuffer(pageSupplier); + Consumer exceptionHandler = (e) -> securityExceptionHandler.accept(httpChannel, e); + + SocketChannelContext context; + if (sslEnabled) { + SSLEngine sslEngine; + boolean hostnameVerificationEnabled = sslConfiguration.verificationMode().isHostnameVerificationEnabled(); + if (hostnameVerificationEnabled) { + InetSocketAddress address = (InetSocketAddress) channel.getRemoteAddress(); + // we create the socket based on the name given. don't reverse DNS + sslEngine = sslService.createSSLEngine(sslConfiguration, address.getHostString(), address.getPort()); + } else { + sslEngine = sslService.createSSLEngine(sslConfiguration, null, -1); + } + SSLDriver sslDriver = new SSLDriver(sslEngine, false); + context = new SSLChannelContext(httpChannel, selector, exceptionHandler, sslDriver, httpHandler, buffer, nioIpFilter); + } else { + context = new BytesChannelContext(httpChannel, selector, exceptionHandler, httpHandler, buffer, nioIpFilter); + } + httpChannel.setContext(context); + + return httpChannel; + } + + @Override + public NioHttpServerChannel createServerChannel(NioSelector selector, ServerSocketChannel channel) { + NioHttpServerChannel httpServerChannel = new NioHttpServerChannel(channel); + Consumer exceptionHandler = (e) -> onServerException(httpServerChannel, e); + Consumer acceptor = SecurityNioHttpServerTransport.this::acceptChannel; + ServerChannelContext context = new ServerChannelContext(httpServerChannel, this, selector, acceptor, exceptionHandler); + httpServerChannel.setContext(context); + + return httpServerChannel; + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java index fb94b669e833b..71e14696a11ff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java @@ -44,7 +44,6 @@ import java.util.Collections; import java.util.Map; import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.function.Supplier; import static org.elasticsearch.xpack.core.security.SecurityField.setting; @@ -129,19 +128,11 @@ protected TcpChannelFactory channelFactory(ProfileSettings profileSettings, bool return new SecurityTcpChannelFactory(profileSettings, isClient); } - private boolean validateChannel(NioSocketChannel channel) { - if (authenticator != null) { - NioTcpChannel nioTcpChannel = (NioTcpChannel) channel; - return authenticator.accept(nioTcpChannel.getProfile(), nioTcpChannel.getRemoteAddress()); - } else { - return true; - } - } - private class SecurityTcpChannelFactory extends TcpChannelFactory { private final String profileName; private final boolean isClient; + private final NioIPFilter ipFilter; private SecurityTcpChannelFactory(ProfileSettings profileSettings, boolean isClient) { super(new RawChannelFactory(profileSettings.tcpNoDelay, @@ -151,12 +142,12 @@ private SecurityTcpChannelFactory(ProfileSettings profileSettings, boolean isCli Math.toIntExact(profileSettings.receiveBufferSize.getBytes()))); this.profileName = profileSettings.profileName; this.isClient = isClient; + this.ipFilter = new NioIPFilter(authenticator, profileName); } @Override public NioTcpChannel createChannel(NioSelector selector, SocketChannel channel) throws IOException { NioTcpChannel nioChannel = new NioTcpChannel(profileName, channel); - SocketChannelContext context; Supplier pageSupplier = () -> { Recycler.V bytes = pageCacheRecycler.bytePage(false); return new InboundChannelBuffer.Page(ByteBuffer.wrap(bytes.v()), bytes::close); @@ -164,8 +155,8 @@ public NioTcpChannel createChannel(NioSelector selector, SocketChannel channel) TcpReadWriteHandler readWriteHandler = new TcpReadWriteHandler(nioChannel, SecurityNioTransport.this); InboundChannelBuffer buffer = new InboundChannelBuffer(pageSupplier); Consumer exceptionHandler = (e) -> onException(nioChannel, e); - Predicate filter = SecurityNioTransport.this::validateChannel; + SocketChannelContext context; if (sslEnabled) { SSLEngine sslEngine; SSLConfiguration defaultConfig = profileConfiguration.get(TcpTransport.DEFAULT_PROFILE); @@ -179,9 +170,9 @@ public NioTcpChannel createChannel(NioSelector selector, SocketChannel channel) sslEngine = sslService.createSSLEngine(sslConfig, null, -1); } SSLDriver sslDriver = new SSLDriver(sslEngine, isClient); - context = new SSLChannelContext(nioChannel, selector, exceptionHandler, sslDriver, readWriteHandler, buffer, filter); + context = new SSLChannelContext(nioChannel, selector, exceptionHandler, sslDriver, readWriteHandler, buffer, ipFilter); } else { - context = new BytesChannelContext(nioChannel, selector, exceptionHandler, readWriteHandler, buffer, filter); + context = new BytesChannelContext(nioChannel, selector, exceptionHandler, readWriteHandler, buffer, ipFilter); } nioChannel.setContext(context); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java index e6db3407496eb..9bb0e44eb664c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java @@ -244,6 +244,7 @@ protected Settings nodeSettings(int nodeOrdinal) { builder.put(customSettings, false); // handle secure settings separately builder.put(LicenseService.SELF_GENERATED_LICENSE_TYPE.getKey(), "trial"); builder.put(NetworkModule.TRANSPORT_TYPE_KEY, randomBoolean() ? SecurityField.NAME4 : SecurityField.NIO); + builder.put(NetworkModule.HTTP_TYPE_KEY, randomBoolean() ? SecurityField.NAME4 : SecurityField.NIO); Settings.Builder customBuilder = Settings.builder().put(customSettings); if (customBuilder.getSecureSettings() != null) { SecuritySettingsSource.addSecureSettings(builder, secureSettings -> diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java index 2e0662264a248..df1456c3790b2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java @@ -126,6 +126,7 @@ public Settings nodeSettings(int nodeOrdinal) { Settings.Builder builder = Settings.builder().put(super.nodeSettings(nodeOrdinal)) .put(XPackSettings.SECURITY_ENABLED.getKey(), true) .put(NetworkModule.TRANSPORT_TYPE_KEY, randomBoolean() ? SecurityField.NAME4 : SecurityField.NIO) + .put(NetworkModule.HTTP_TYPE_KEY, randomBoolean() ? SecurityField.NAME4 : SecurityField.NIO) //TODO: for now isolate security tests from watcher & monitoring (randomize this later) .put(XPackSettings.WATCHER_ENABLED.getKey(), false) .put(XPackSettings.MONITORING_ENABLED.getKey(), false) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettingsTests.java new file mode 100644 index 0000000000000..56c79a4c12791 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityHttpSettingsTests.java @@ -0,0 +1,44 @@ +/* + * 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.security.transport; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.http.HttpTransportSettings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackSettings; + +import static org.hamcrest.Matchers.is; + +public class SecurityHttpSettingsTests extends ESTestCase { + + public void testDisablesCompressionByDefaultForSsl() { + Settings settings = Settings.builder() + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); + + Settings.Builder pluginSettingsBuilder = Settings.builder(); + SecurityHttpSettings.overrideSettings(pluginSettingsBuilder, settings); + assertThat(HttpTransportSettings.SETTING_HTTP_COMPRESSION.get(pluginSettingsBuilder.build()), is(false)); + } + + public void testLeavesCompressionOnIfNotSsl() { + Settings settings = Settings.builder() + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), false).build(); + Settings.Builder pluginSettingsBuilder = Settings.builder(); + SecurityHttpSettings.overrideSettings(pluginSettingsBuilder, settings); + assertThat(pluginSettingsBuilder.build().isEmpty(), is(true)); + } + + public void testDoesNotChangeExplicitlySetCompression() { + Settings settings = Settings.builder() + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put(HttpTransportSettings.SETTING_HTTP_COMPRESSION.getKey(), true) + .build(); + + Settings.Builder pluginSettingsBuilder = Settings.builder(); + SecurityHttpSettings.overrideSettings(pluginSettingsBuilder, settings); + assertThat(pluginSettingsBuilder.build().isEmpty(), is(true)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java index ec925f43abe79..ad64dea79a587 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.http.NullDispatcher; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -144,34 +143,6 @@ public void testCustomSSLConfiguration() throws Exception { assertThat(customEngine.getEnabledProtocols(), not(equalTo(defaultEngine.getEnabledProtocols()))); } - public void testDisablesCompressionByDefaultForSsl() throws Exception { - Settings settings = Settings.builder() - .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); - - Settings.Builder pluginSettingsBuilder = Settings.builder(); - SecurityNetty4HttpServerTransport.overrideSettings(pluginSettingsBuilder, settings); - assertThat(HttpTransportSettings.SETTING_HTTP_COMPRESSION.get(pluginSettingsBuilder.build()), is(false)); - } - - public void testLeavesCompressionOnIfNotSsl() throws Exception { - Settings settings = Settings.builder() - .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), false).build(); - Settings.Builder pluginSettingsBuilder = Settings.builder(); - SecurityNetty4HttpServerTransport.overrideSettings(pluginSettingsBuilder, settings); - assertThat(pluginSettingsBuilder.build().isEmpty(), is(true)); - } - - public void testDoesNotChangeExplicitlySetCompression() throws Exception { - Settings settings = Settings.builder() - .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) - .put(HttpTransportSettings.SETTING_HTTP_COMPRESSION.getKey(), true) - .build(); - - Settings.Builder pluginSettingsBuilder = Settings.builder(); - SecurityNetty4HttpServerTransport.overrideSettings(pluginSettingsBuilder, settings); - assertThat(pluginSettingsBuilder.build().isEmpty(), is(true)); - } - public void testThatExceptionIsThrownWhenConfiguredWithoutSslKey() throws Exception { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java new file mode 100644 index 0000000000000..1832669fce144 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java @@ -0,0 +1,91 @@ +/* + * 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.security.transport.nio; + +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.nio.NioSocketChannel; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.transport.filter.IPFilter; +import org.junit.Before; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NioIPFilterTests extends ESTestCase { + + private NioIPFilter nioIPFilter; + + @Before + public void init() throws Exception { + Settings settings = Settings.builder() + .put("xpack.security.transport.filter.allow", "127.0.0.1") + .put("xpack.security.transport.filter.deny", "10.0.0.0/8") + .build(); + + boolean isHttpEnabled = randomBoolean(); + + Transport transport = mock(Transport.class); + TransportAddress address = new TransportAddress(InetAddress.getLoopbackAddress(), 9300); + when(transport.boundAddress()).thenReturn(new BoundTransportAddress(new TransportAddress[] { address }, address)); + when(transport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(Arrays.asList( + IPFilter.HTTP_FILTER_ALLOW_SETTING, + IPFilter.HTTP_FILTER_DENY_SETTING, + IPFilter.IP_FILTER_ENABLED_HTTP_SETTING, + IPFilter.IP_FILTER_ENABLED_SETTING, + IPFilter.TRANSPORT_FILTER_ALLOW_SETTING, + IPFilter.TRANSPORT_FILTER_DENY_SETTING, + IPFilter.PROFILE_FILTER_ALLOW_SETTING, + IPFilter.PROFILE_FILTER_DENY_SETTING))); + XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isIpFilteringAllowed()).thenReturn(true); + when(licenseState.isSecurityEnabled()).thenReturn(true); + AuditTrailService auditTrailService = new AuditTrailService(settings, Collections.emptyList(), licenseState); + IPFilter ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); + ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); + if (isHttpEnabled) { + HttpServerTransport httpTransport = mock(HttpServerTransport.class); + TransportAddress httpAddress = new TransportAddress(InetAddress.getLoopbackAddress(), 9200); + when(httpTransport.boundAddress()).thenReturn(new BoundTransportAddress(new TransportAddress[] { httpAddress }, httpAddress)); + when(httpTransport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); + ipFilter.setBoundHttpTransportAddress(httpTransport.boundAddress()); + } + + if (isHttpEnabled) { + nioIPFilter = new NioIPFilter(ipFilter, IPFilter.HTTP_PROFILE_NAME); + } else { + nioIPFilter = new NioIPFilter(ipFilter, "default"); + } + } + + public void testThatFilteringWorksByIp() throws Exception { + InetSocketAddress localhostAddr = new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 12345); + NioSocketChannel channel1 = mock(NioSocketChannel.class); + when(channel1.getRemoteAddress()).thenReturn(localhostAddr); + assertThat(nioIPFilter.test(channel1), is(true)); + + InetSocketAddress remoteAddr = new InetSocketAddress(InetAddresses.forString("10.0.0.8"), 12345); + NioSocketChannel channel2 = mock(NioSocketChannel.class); + when(channel2.getRemoteAddress()).thenReturn(remoteAddr); + assertThat(nioIPFilter.test(channel2), is(false)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java new file mode 100644 index 0000000000000..b5d84d459160f --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java @@ -0,0 +1,207 @@ +/* + * 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.security.transport.nio; + +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.http.NullDispatcher; +import org.elasticsearch.http.nio.NioHttpChannel; +import org.elasticsearch.nio.NioSelector; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ssl.SSLClientAuth; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.transport.SSLEngineUtils; +import org.elasticsearch.xpack.security.transport.filter.IPFilter; +import org.junit.Before; + +import javax.net.ssl.SSLEngine; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Locale; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SecurityNioHttpServerTransportTests extends ESTestCase { + + private SSLService sslService; + private Environment env; + private InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + + @Before + public void createSSLService() { + Path testNodeStore = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks"); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.keystore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put("xpack.ssl.keystore.path", testNodeStore) + .put("path.home", createTempDir()) + .setSecureSettings(secureSettings) + .build(); + env = TestEnvironment.newEnvironment(settings); + sslService = new SSLService(settings, env); + } + + public void testDefaultClientAuth() throws IOException { + Settings settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.getRemoteAddress()).thenReturn(address); + NioHttpChannel channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine engine = SSLEngineUtils.getSSLEngine(channel); + + assertThat(engine.getNeedClientAuth(), is(false)); + assertThat(engine.getWantClientAuth(), is(false)); + } + + public void testOptionalClientAuth() throws IOException { + String value = randomFrom(SSLClientAuth.OPTIONAL.name(), SSLClientAuth.OPTIONAL.name().toLowerCase(Locale.ROOT)); + Settings settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put("xpack.security.http.ssl.client_authentication", value).build(); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + + SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.getRemoteAddress()).thenReturn(address); + NioHttpChannel channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine engine = SSLEngineUtils.getSSLEngine(channel); + assertThat(engine.getNeedClientAuth(), is(false)); + assertThat(engine.getWantClientAuth(), is(true)); + } + + public void testRequiredClientAuth() throws IOException { + String value = randomFrom(SSLClientAuth.REQUIRED.name(), SSLClientAuth.REQUIRED.name().toLowerCase(Locale.ROOT)); + Settings settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put("xpack.security.http.ssl.client_authentication", value).build(); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + + SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.getRemoteAddress()).thenReturn(address); + NioHttpChannel channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine engine = SSLEngineUtils.getSSLEngine(channel); + assertThat(engine.getNeedClientAuth(), is(true)); + assertThat(engine.getWantClientAuth(), is(false)); + } + + public void testNoClientAuth() throws IOException { + String value = randomFrom(SSLClientAuth.NONE.name(), SSLClientAuth.NONE.name().toLowerCase(Locale.ROOT)); + Settings settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put("xpack.security.http.ssl.client_authentication", value).build(); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + + SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.getRemoteAddress()).thenReturn(address); + NioHttpChannel channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine engine = SSLEngineUtils.getSSLEngine(channel); + assertThat(engine.getNeedClientAuth(), is(false)); + assertThat(engine.getWantClientAuth(), is(false)); + } + + public void testCustomSSLConfiguration() throws IOException { + Settings settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.getRemoteAddress()).thenReturn(address); + NioHttpChannel channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine defaultEngine = SSLEngineUtils.getSSLEngine(channel); + + settings = Settings.builder() + .put(env.settings()) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put("xpack.security.http.ssl.supported_protocols", "TLSv1.2") + .build(); + sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + factory = transport.channelFactory(); + channel = factory.createChannel(mock(NioSelector.class), socketChannel); + SSLEngine customEngine = SSLEngineUtils.getSSLEngine(channel); + assertThat(customEngine.getEnabledProtocols(), arrayContaining("TLSv1.2")); + assertThat(customEngine.getEnabledProtocols(), not(equalTo(defaultEngine.getEnabledProtocols()))); + } + + public void testThatExceptionIsThrownWhenConfiguredWithoutSslKey() { + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put("xpack.ssl.truststore.path", + getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks")) + .setSecureSettings(secureSettings) + .put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true) + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(settings); + sslService = new SSLService(settings, env); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService)); + assertThat(e.getMessage(), containsString("key must be provided")); + } + + public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() { + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.ssl.truststore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put("xpack.ssl.truststore.path", + getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks")) + .setSecureSettings(secureSettings) + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(settings); + sslService = new SSLService(settings, env); + SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, + new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), + xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService); + } +}