Added SSL support in netty
This introduces the possibility to have all communications (transport and HTTP) to run over SSL. Original commit: elastic/x-pack-elasticsearch@c816a65f53
This commit is contained in:
parent
bc0e233589
commit
d0673b0cfb
|
@ -4,5 +4,37 @@ This plugins adds security features to elasticsearch
|
|||
|
||||
== Access control
|
||||
|
||||
== Encrypted communication
|
||||
== Encrypted communication using TLS/SSL
|
||||
|
||||
=== Configuration parameters
|
||||
|
||||
==== Transport protocol
|
||||
|
||||
* `transport.tcp.ssl`: true|false (defaults to true)
|
||||
* `transport.tcp.ssl.keystore`: /path/to/the/keystore (absolute path to the keystore, which contains private keys)
|
||||
* `transport.tcp.ssl.keystore_password`: password of the keystore
|
||||
* `transport.tcp.ssl.keystore_algorithm`: keystore format (defaults to SunX509)
|
||||
* `transport.tcp.ssl.truststore`: /path/to/the/truststore (absolute path to the truststore, which contains trusted keys)
|
||||
* `transport.tcp.ssl.truststore_password`: password of the truststore
|
||||
* `transport.tcp.ssl.truststore_algorithm`: truststore format (defaults to SunX509)
|
||||
* `transport.tcp.ssl.client.auth`: true|false (defaults to true)
|
||||
* `transport.tcp.ssl.ciphers`: Supported ciphers, defaults to `TLS_RSA_WITH_AES_128_CBC_SHA256` and `TLS_RSA_WITH_AES_128_CBC_SHA`
|
||||
|
||||
==== HTTP
|
||||
|
||||
* `http.ssl`: true|false (defaults to true)
|
||||
* `http.ssl.keystore`: /path/to/the/keystore (absolute path to the keystore, which contains private keys)
|
||||
* `http.ssl.keystore_password`: password of the keystore
|
||||
* `http.ssl.keystore_algorithm`: keystore format (defaults to SunX509)
|
||||
* `http.ssl.truststore`: /path/to/the/truststore (absolute path to the truststore, which contains trusted keys)
|
||||
* `http.ssl.truststore_password`: password of the truststore
|
||||
* `http.ssl.truststore_algorithm`: truststore format (defaults to SunX509)
|
||||
* `http.ssl.client.auth`: true|false (defaults to true)
|
||||
* `http.ssl.ciphers`: Supported ciphers, defaults to `TLS_RSA_WITH_AES_128_CBC_SHA256` and `TLS_RSA_WITH_AES_128_CBC_SHA`
|
||||
|
||||
== Generating certificates
|
||||
|
||||
=== Using self signed certificates per node
|
||||
|
||||
=== Using an own CA
|
||||
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -77,6 +77,13 @@
|
|||
<version>2.1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
<version>1.2.17</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- real dependencies -->
|
||||
|
||||
|
|
|
@ -29,6 +29,11 @@ public class SecurityModule extends AbstractModule implements SpawnModules {
|
|||
|
||||
@Override
|
||||
public Iterable<? extends Module> spawnModules() {
|
||||
// dont spawn module in client mode
|
||||
if (settings.getAsBoolean("node.client", false)) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
return ImmutableList.of(
|
||||
Modules.createModule(AuthenticationModule.class, settings),
|
||||
Modules.createModule(AuthorizationModule.class, settings),
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.shield.ssl;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ElasticsearchSSLException extends ElasticsearchException {
|
||||
|
||||
public ElasticsearchSSLException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ElasticsearchSSLException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestStatus status() {
|
||||
return RestStatus.BAD_REQUEST;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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.shield.ssl;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SSLConfig {
|
||||
|
||||
private static final ESLogger logger = Loggers.getLogger(SSLConfig.class);
|
||||
// TODO removing the second one results in fails, need to verify the differences, maybe per JVM?
|
||||
public static final String[] DEFAULT_CIPHERS = new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA" };
|
||||
private final boolean clientAuth;
|
||||
|
||||
private SSLContext sslContext;
|
||||
private String[] ciphers;
|
||||
|
||||
public SSLConfig(Settings componentSettings) {
|
||||
this(componentSettings, ImmutableSettings.EMPTY);
|
||||
}
|
||||
|
||||
public SSLConfig(Settings componentSettings, Settings settings) {
|
||||
this.clientAuth = componentSettings.getAsBoolean("client.auth", settings.getAsBoolean("ssl.client.auth", true));
|
||||
String keyStore = componentSettings.get("keystore", settings.get("ssl.keystore", System.getProperty("javax.net.ssl.keyStore")));
|
||||
String keyStorePassword = componentSettings.get("keystore_password", settings.get("ssl.keystore_password", System.getProperty("javax.net.ssl.keyStorePassword")));
|
||||
String keyStoreAlgorithm = componentSettings.get("keystore_algorithm", settings.get("ssl.keystore_algorithm", System.getProperty("ssl.KeyManagerFactory.algorithm")));
|
||||
String trustStore = componentSettings.get("truststore", settings.get("ssl.truststore", System.getProperty("javax.net.ssl.trustStore")));
|
||||
String trustStorePassword = componentSettings.get("truststore_password", settings.get("ssl.truststore_password", System.getProperty("javax.net.ssl.trustStorePassword")));
|
||||
String trustStoreAlgorithm = componentSettings.get("truststore_algorithm", settings.get("ssl.truststore_algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm")));
|
||||
this.ciphers = componentSettings.getAsArray("ciphers", settings.getAsArray("ssl.ciphers", DEFAULT_CIPHERS));
|
||||
|
||||
if (keyStoreAlgorithm == null) {
|
||||
keyStoreAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
|
||||
}
|
||||
|
||||
if (trustStoreAlgorithm == null) {
|
||||
trustStoreAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
|
||||
}
|
||||
|
||||
logger.debug("using keyStore[{}], keyAlgorithm[{}], trustStore[{}], trustAlgorithm[{}]", keyStore, keyStoreAlgorithm, trustStore, trustStoreAlgorithm);
|
||||
|
||||
KeyStore ks = null;
|
||||
KeyManagerFactory kmf = null;
|
||||
try (FileInputStream in = new FileInputStream(keyStore)){
|
||||
// Load KeyStore
|
||||
ks = KeyStore.getInstance("jks");
|
||||
ks.load(in, keyStorePassword.toCharArray());
|
||||
|
||||
// Initialize KeyManagerFactory
|
||||
kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm);
|
||||
kmf.init(ks, keyStorePassword.toCharArray());
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchException("Failed to initialize a KeyManagerFactory", e);
|
||||
}
|
||||
|
||||
TrustManager[] trustManagers = null;
|
||||
try (FileInputStream in = new FileInputStream(trustStore)) {
|
||||
// Load TrustStore
|
||||
ks.load(in, trustStorePassword.toCharArray());
|
||||
|
||||
// Initialize a trust manager factory with the trusted store
|
||||
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
|
||||
trustFactory.init(ks);
|
||||
|
||||
// Retrieve the trust managers from the factory
|
||||
trustManagers = trustFactory.getTrustManagers();
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchException("Failed to initialize a TrustManagerFactory", e);
|
||||
}
|
||||
|
||||
// Initialize sslContext
|
||||
try {
|
||||
sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
|
||||
} catch (Exception e) {
|
||||
throw new Error("Failed to initialize the SSLContext", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SSLEngine createSSLEngine() {
|
||||
SSLEngine sslEngine = sslContext.createSSLEngine();
|
||||
try {
|
||||
sslEngine.setEnabledCipherSuites(ciphers);
|
||||
} catch (Throwable t) {
|
||||
throw new ElasticsearchSSLException("Error loading cipher suites ["+Arrays.asList(ciphers)+"]", t);
|
||||
}
|
||||
sslEngine.setNeedClientAuth(clientAuth);
|
||||
return sslEngine;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.shield.ssl.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipeline;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipelineFactory;
|
||||
import org.elasticsearch.common.netty.handler.ssl.SslHandler;
|
||||
import org.elasticsearch.common.network.NetworkService;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.BigArrays;
|
||||
import org.elasticsearch.http.netty.NettyHttpServerTransport;
|
||||
import org.elasticsearch.shield.ssl.SSLConfig;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySSLHttpServerTransport extends NettyHttpServerTransport {
|
||||
|
||||
private final boolean ssl;
|
||||
|
||||
@Inject
|
||||
public NettySSLHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays) {
|
||||
super(settings, networkService, bigArrays);
|
||||
this.ssl = settings.getAsBoolean("http.ssl", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipelineFactory configureServerChannelPipelineFactory() {
|
||||
return new HttpSslChannelPipelineFactory(this);
|
||||
}
|
||||
|
||||
private class HttpSslChannelPipelineFactory extends HttpChannelPipelineFactory {
|
||||
|
||||
public HttpSslChannelPipelineFactory(NettyHttpServerTransport transport) {
|
||||
super(transport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipeline getPipeline() throws Exception {
|
||||
ChannelPipeline pipeline = super.getPipeline();
|
||||
if (ssl) {
|
||||
SSLConfig sslConfig = new SSLConfig(settings.getByPrefix("http.ssl."));
|
||||
SSLEngine engine = sslConfig.createSSLEngine();
|
||||
engine.setUseClientMode(false);
|
||||
// TODO MAKE ME CONFIGURABLE
|
||||
engine.setNeedClientAuth(false);
|
||||
pipeline.addFirst("ssl", new SslHandler(engine));
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.shield.ssl.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.http.HttpServerTransport;
|
||||
import org.elasticsearch.http.netty.NettyHttpServerTransport;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySSLHttpServerTransportModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(HttpServerTransport.class).to(NettySSLHttpServerTransport.class).asEagerSingleton();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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.shield.ssl.netty;
|
||||
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipeline;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipelineFactory;
|
||||
import org.elasticsearch.common.netty.handler.ssl.SslHandler;
|
||||
import org.elasticsearch.common.network.NetworkService;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.BigArrays;
|
||||
import org.elasticsearch.shield.ssl.SSLConfig;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.netty.NettyTransport;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySSLTransport extends NettyTransport {
|
||||
|
||||
private final boolean ssl;
|
||||
|
||||
@Inject
|
||||
public NettySSLTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version) {
|
||||
super(settings, threadPool, networkService, bigArrays, version);
|
||||
this.ssl = settings.getAsBoolean("transport.tcp.ssl", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipelineFactory configureClientChannelPipelineFactory() {
|
||||
return new SslClientChannelPipelineFactory(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipelineFactory configureServerChannelPipelineFactory() {
|
||||
return new SslServerChannelPipelineFactory(this);
|
||||
}
|
||||
|
||||
private class SslServerChannelPipelineFactory extends ServerChannelPipeFactory {
|
||||
|
||||
private final SSLConfig sslConfig;
|
||||
|
||||
public SslServerChannelPipelineFactory(NettyTransport nettyTransport) {
|
||||
super(nettyTransport);
|
||||
sslConfig = new SSLConfig(settings.getByPrefix("transport.tcp.ssl."));
|
||||
// try to create an SSL engine, so that exceptions lead to early exit
|
||||
sslConfig.createSSLEngine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipeline getPipeline() throws Exception {
|
||||
ChannelPipeline pipeline = super.getPipeline();
|
||||
if (ssl) {
|
||||
SSLEngine serverEngine = sslConfig.createSSLEngine();
|
||||
serverEngine.setUseClientMode(false);
|
||||
|
||||
pipeline.addFirst("ssl", new SslHandler(serverEngine));
|
||||
pipeline.replace("dispatcher", "dispatcher", new SecureMessageChannelHandler(nettyTransport, logger));
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
private class SslClientChannelPipelineFactory extends ClientChannelPipelineFactory {
|
||||
|
||||
private final SSLConfig sslConfig;
|
||||
|
||||
public SslClientChannelPipelineFactory(NettyTransport transport) {
|
||||
super(transport);
|
||||
sslConfig = new SSLConfig(settings.getByPrefix("transport.tcp.ssl."));
|
||||
// try to create an SSL engine, so that exceptions lead to early exit
|
||||
sslConfig.createSSLEngine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipeline getPipeline() throws Exception {
|
||||
ChannelPipeline pipeline = super.getPipeline();
|
||||
if (ssl) {
|
||||
SSLEngine clientEngine = sslConfig.createSSLEngine();
|
||||
clientEngine.setUseClientMode(true);
|
||||
|
||||
pipeline.addFirst("ssl", new SslHandler(clientEngine));
|
||||
pipeline.replace("dispatcher", "dispatcher", new SecureMessageChannelHandler(nettyTransport, logger));
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.shield.ssl.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySSLTransportModule extends AbstractModule {
|
||||
|
||||
private final Settings settings;
|
||||
|
||||
public NettySSLTransportModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(NettySSLTransport.class).asEagerSingleton();
|
||||
bind(Transport.class).to(NettySSLTransport.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.shield.ssl.netty;
|
||||
|
||||
import org.elasticsearch.shield.ssl.ElasticsearchSSLException;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.netty.channel.ChannelFuture;
|
||||
import org.elasticsearch.common.netty.channel.ChannelFutureListener;
|
||||
import org.elasticsearch.common.netty.channel.ChannelHandlerContext;
|
||||
import org.elasticsearch.common.netty.channel.ChannelStateEvent;
|
||||
import org.elasticsearch.common.netty.handler.ssl.SslHandler;
|
||||
import org.elasticsearch.transport.netty.MessageChannelHandler;
|
||||
|
||||
public class SecureMessageChannelHandler extends MessageChannelHandler {
|
||||
|
||||
public SecureMessageChannelHandler(org.elasticsearch.transport.netty.NettyTransport transport, ESLogger logger) {
|
||||
super(transport, logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelConnected(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
|
||||
SslHandler sslHandler = ctx.getPipeline().get(SslHandler.class);
|
||||
|
||||
// Get notified when SSL handshake is done.
|
||||
final ChannelFuture handshakeFuture = sslHandler.handshake();
|
||||
handshakeFuture.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
if (future.isSuccess()) {
|
||||
logger.debug("SSL / TLS handshake completed for the channel.");
|
||||
ctx.sendUpstream(e);
|
||||
} else {
|
||||
logger.error("SSL / TLS handshake failed, closing the channel");
|
||||
future.getChannel().close();
|
||||
throw new ElasticsearchSSLException("SSL / TLS handshake failed, closing the channel", future.getCause());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
|
|||
/**
|
||||
*
|
||||
*/
|
||||
@ClusterScope(scope = Scope.SUITE)
|
||||
@ClusterScope(scope = Scope.SUITE, numDataNodes = 2)
|
||||
public class ShieldPluginTests extends ElasticsearchIntegrationTest {
|
||||
|
||||
@Override
|
||||
|
@ -36,7 +36,9 @@ public class ShieldPluginTests extends ElasticsearchIntegrationTest {
|
|||
@Test
|
||||
@TestLogging("_root:INFO,plugins.PluginsService:TRACE")
|
||||
public void testThatPluginIsLoaded() {
|
||||
NodesInfoResponse nodeInfos = internalCluster().clientNodeClient().admin().cluster().prepareNodesInfo().get();
|
||||
logger.info("--> Getting nodes info");
|
||||
NodesInfoResponse nodeInfos = internalCluster().transportClient().admin().cluster().prepareNodesInfo().get();
|
||||
logger.info("--> Checking nodes info");
|
||||
for (NodeInfo nodeInfo : nodeInfos.getNodes()) {
|
||||
assertThat(nodeInfo.getPlugins().getInfos(), hasSize(1));
|
||||
assertThat(nodeInfo.getPlugins().getInfos().get(0).getName(), is("shield"));
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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.shield.ssl;
|
||||
|
||||
import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.base.Charsets;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import org.elasticsearch.shield.ssl.ElasticsearchSSLException;
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.http.HttpServerTransport;
|
||||
import org.elasticsearch.node.Node;
|
||||
import org.elasticsearch.node.NodeBuilder;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.ssl.netty.NettySSLHttpServerTransportModule;
|
||||
import org.elasticsearch.shield.ssl.netty.NettySSLTransportModule;
|
||||
import org.elasticsearch.test.ElasticsearchIntegrationTest;
|
||||
import org.elasticsearch.test.junit.annotations.TestLogging;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
import org.elasticsearch.transport.TransportModule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
|
||||
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
|
||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
* Created a testnode cert and a test client cert, which is imported into the keystore
|
||||
*
|
||||
* keytool -genkeypair -alias testnode -keystore testnode.jks -keyalg RSA -storepass testnode -keypass testnode -dname "cn=Elasticsearch Test Node, ou=elasticsearch, o=org"
|
||||
* keytool -export -alias testnode -keystore testnode.jks -rfc -file testnode.cert -storepass testnode
|
||||
*
|
||||
* keytool -genkeypair -alias testclient -keystore testclient.jks -keyalg RSA -storepass testclient -keypass testclient -dname "cn=Elasticsearch Test Client, ou=elasticsearch, o=org"
|
||||
* keytool -export -alias testclient -keystore testclient.jks -rfc -file testclient.cert -storepass testclient
|
||||
*
|
||||
* keytool -import -trustcacerts -alias testclient -file testclient.cert -keystore testnode.jks -storepass testnode -noprompt
|
||||
* keytool -import -trustcacerts -alias testnode -file testnode.cert -keystore testclient.jks -storepass testclient -noprompt
|
||||
*
|
||||
*/
|
||||
@ClusterScope(scope = Scope.SUITE, numDataNodes = 1, transportClientRatio = 0.0, numClientNodes = 0)
|
||||
public class SslIntegrationTests extends ElasticsearchIntegrationTest {
|
||||
|
||||
/*
|
||||
# transport.tcp.ssl.keystore: /path/to/the/keystore
|
||||
# transport.tcp.ssl.keystore_password: password
|
||||
# transport.tcp.ssl.keystore_algorithm: SunX509
|
||||
#
|
||||
# transport.tcp.ssl.truststore: /path/to/the/truststore
|
||||
# transport.tcp.ssl.truststore_password: password
|
||||
# transport.tcp.ssl.truststore_algorithm: PKIX
|
||||
*/
|
||||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
File testnodeStore;
|
||||
try {
|
||||
testnodeStore = new File(getClass().getResource("/certs/simple/testnode.jks").toURI());
|
||||
assertThat(testnodeStore.exists(), is(true));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return ImmutableSettings.settingsBuilder()
|
||||
.put(super.nodeSettings(nodeOrdinal))
|
||||
.put("discovery.zen.ping.multicast.ping.enabled", false)
|
||||
// needed to ensure that netty transport is started
|
||||
.put("node.mode", "network")
|
||||
.put("transport.tcp.ssl", true)
|
||||
.put("transport.tcp.ssl.keystore", testnodeStore.getPath())
|
||||
.put("transport.tcp.ssl.keystore_password", "testnode")
|
||||
.put("transport.tcp.ssl.truststore", testnodeStore.getPath())
|
||||
.put("transport.tcp.ssl.truststore_password", "testnode")
|
||||
.put("http.ssl", true)
|
||||
.put("http.ssl.keystore", testnodeStore.getPath())
|
||||
.put("http.ssl.keystore_password", "testnode")
|
||||
.put("http.ssl.truststore", testnodeStore.getPath())
|
||||
.put("http.ssl.truststore_password", "testnode")
|
||||
// SSL SETUP
|
||||
.put("http.type", NettySSLHttpServerTransportModule.class.getName())
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySSLTransportModule.class.getName())
|
||||
.put("plugin.types", SecurityPlugin.class.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
System.setProperty("javax.net.debug", "all");
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
System.clearProperty("javax.net.debug");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestLogging("_root:INFO,org.elasticsearch.test:TRACE, org.elasticsearch.client.transport:DEBUG,org.elasticsearch.shield:TRACE")
|
||||
public void testThatTransportClientCanConnectToNodeViaSsl() throws Exception {
|
||||
TransportClient transportClient = new TransportClient(getSettings("transport_client").build(), false);
|
||||
TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress();
|
||||
transportClient.addTransportAddress(transportAddress);
|
||||
|
||||
assertGreenClusterState(transportClient);
|
||||
}
|
||||
|
||||
@Test(expected = ElasticsearchSSLException.class)
|
||||
@TestLogging("_root:INFO,org.elasticsearch.client.transport:DEBUG")
|
||||
public void testThatUnconfiguredCipchersAreRejected() {
|
||||
// some randomly taken ciphers from SSLContext.getDefault().getSocketFactory().getSupportedCipherSuites()
|
||||
// could be really randomized
|
||||
Settings customSettings = getSettings("transport_client").put("transport.tcp.ssl.ciphers", new String[]{"TLS_ECDH_anon_WITH_RC4_128_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA"}).build();
|
||||
|
||||
TransportClient transportClient = new TransportClient(customSettings);
|
||||
|
||||
TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress();
|
||||
transportClient.addTransportAddress(transportAddress);
|
||||
|
||||
transportClient.admin().cluster().prepareHealth().get();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectNodeWorks() throws Exception {
|
||||
try (Node node = NodeBuilder.nodeBuilder().settings(getSettings("ssl_node")).node().start()) {
|
||||
try (Client client = node.client()) {
|
||||
assertGreenClusterState(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectNodeClientWorks() throws Exception {
|
||||
// no multicast, good old discovery
|
||||
TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress();
|
||||
assertThat(transportAddress, is(instanceOf(InetSocketTransportAddress.class)));
|
||||
InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) transportAddress;
|
||||
Settings.Builder settingsBuilder = getSettings("node_client")
|
||||
.put("node.client", true)
|
||||
.put("discovery.zen.ping.multicast.ping.enabled", false)
|
||||
.put("discovery.zen.ping.unicast.hosts", inetSocketTransportAddress.address().getHostString() + ":" + inetSocketTransportAddress.address().getPort());
|
||||
|
||||
try (Node node = NodeBuilder.nodeBuilder().settings(settingsBuilder).node().start()) {
|
||||
try (Client client = node.client()) {
|
||||
assertGreenClusterState(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ElasticsearchSSLException.class)
|
||||
public void testConnectNodeFailsWithWrongCipher() throws Exception {
|
||||
Settings customSettings = getSettings("ssl_node").put("transport.tcp.ssl.ciphers", new String[]{"TLS_ECDH_anon_WITH_RC4_128_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA"}).build();
|
||||
NodeBuilder.nodeBuilder().settings(customSettings).node().start();
|
||||
}
|
||||
|
||||
@Test(expected = ElasticsearchSSLException.class)
|
||||
public void testConnectNodeClientFailsWithWrongCipher() throws Exception {
|
||||
Settings customSettings = getSettings("ssl_node").put("node.client", true).put("transport.tcp.ssl.ciphers", new String[]{"TLS_ECDH_anon_WITH_RC4_128_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA"}).build();
|
||||
NodeBuilder.nodeBuilder().settings(customSettings).node().start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatConnectionToHTTPWorks() throws Exception {
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void checkClientTrusted(
|
||||
java.security.cert.X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(
|
||||
java.security.cert.X509Certificate[] certs, String authType) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||
// totally secure
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String s, SSLSession sslSession) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
TransportAddress transportAddress = internalCluster().getInstance(HttpServerTransport.class).boundAddress().boundAddress();
|
||||
assertThat(transportAddress, is(instanceOf(InetSocketTransportAddress.class)));
|
||||
InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) transportAddress;
|
||||
String url = String.format(Locale.ROOT, "https://%s:%s/", InetAddresses.toUriString(inetSocketTransportAddress.address().getAddress()), inetSocketTransportAddress.address().getPort());
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.connect();
|
||||
|
||||
assertThat(connection.getResponseCode(), is(200));
|
||||
String data = Streams.copyToString(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8));
|
||||
assertThat(data, containsString("You Know, for Search"));
|
||||
}
|
||||
|
||||
private ImmutableSettings.Builder getSettings(String name) {
|
||||
File testClientKeyStore;
|
||||
File testClientTrustStore;
|
||||
try {
|
||||
testClientKeyStore = new File(getClass().getResource("/certs/simple/testclient.jks").toURI());
|
||||
testClientTrustStore = new File(getClass().getResource("/certs/simple/testclient.jks").toURI());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
assertThat(testClientKeyStore.exists(), is(true));
|
||||
assertThat(testClientTrustStore.exists(), is(true));
|
||||
|
||||
return ImmutableSettings.settingsBuilder()
|
||||
.put("node.name", name)
|
||||
.put("transport.tcp.ssl", true)
|
||||
.put("transport.tcp.ssl.keystore", testClientKeyStore.getPath())
|
||||
.put("transport.tcp.ssl.keystore_password", "testclient")
|
||||
.put("transport.tcp.ssl.truststore", testClientTrustStore .getPath())
|
||||
.put("transport.tcp.ssl.truststore_password", "testclient")
|
||||
.put("discovery.zen.ping.multicast.ping.enabled", false)
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySSLTransportModule.class.getName())
|
||||
//.put("plugin.types", SecurityPlugin.class.getName())
|
||||
.put("cluster.name", internalCluster().getClusterName());
|
||||
}
|
||||
|
||||
private void assertGreenClusterState(Client client) {
|
||||
ClusterHealthResponse clusterHealthResponse = client.admin().cluster().prepareHealth().get();
|
||||
assertNoTimeout(clusterHealthResponse);
|
||||
assertThat(clusterHealthResponse.getStatus(), is(ClusterHealthStatus.GREEN));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDMzCCAhugAwIBAgIEIqIBljANBgkqhkiG9w0BAQsFADBKMQwwCgYDVQQKEwNvcmcxFjAUBgNV
|
||||
BAsTDWVsYXN0aWNzZWFyY2gxIjAgBgNVBAMTGUVsYXN0aWNzZWFyY2ggVGVzdCBDbGllbnQwHhcN
|
||||
MTQwNzIyMDc1MzA1WhcNMTQxMDIwMDc1MzA1WjBKMQwwCgYDVQQKEwNvcmcxFjAUBgNVBAsTDWVs
|
||||
YXN0aWNzZWFyY2gxIjAgBgNVBAMTGUVsYXN0aWNzZWFyY2ggVGVzdCBDbGllbnQwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZTqtl7W0u/e7GAqyFhbC4DLlBByH7B0ZwPdh/ulVfIzDi
|
||||
Er7zkOGQtxkrsWHQZmfeyroLwKhfIaA69Jvd+JRFqn53zW3iziduXeMs9qnNP6Mb1ePmBmrs6icU
|
||||
J1Wcxz5O+JofJ8g3+9KWPHVS4Ls1or43W14fV5LAnSt5lOhC33ayawUstls3mdb2LUvqkgZTwFxd
|
||||
SwRAB6o6x+5JLrA3p2sVh97C4PyC5BxfR7EWfR6/gc7BvcqP9yxfKPf4TmaufStNQ3ESoH5wzGUy
|
||||
i/XWUy0GYjaqk+DM1RTjhVetDr2jPl2akILqA0W3pTtjutsVxmN0W8EN9hGBlCWHyxjHAgMBAAGj
|
||||
ITAfMB0GA1UdDgQWBBR15qT6iIEGeeeE7IMY9ZxGhDQHWzANBgkqhkiG9w0BAQsFAAOCAQEAdhic
|
||||
qBw2TqnkOZOtRCxt5u4DnZE63i1W//kuFEjf0ee6/PFpPlKLROCzBCnf/ys49ixg6Q50H6U0SKwL
|
||||
bMhdyAqfZakEknR8uTpvWP3WA+BcCuUB2drlvd3DWI+FF889otOwacNOrwyHnrN7Uv18Hh9PPcyD
|
||||
x3V9uZSWzIQ4hU2+Fe0HHLenxIUFh6nqfCBy+81+AKQQuN4vE7TRUq/2WIK5rmwB8pakEoyc15ks
|
||||
i/NGa1GjUvfM8G4FprINJ+Xrg6kcC7SL5pJVMCA+8c/iMPm22/XOYj7m4UL9BXKW9FFJX+3AMVZm
|
||||
I4xHtBAZU4nkbnvDrbmsQvzGAp565LBv1g==
|
||||
-----END CERTIFICATE-----
|
Binary file not shown.
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDLzCCAhegAwIBAgIEQv9ZtTANBgkqhkiG9w0BAQsFADBIMQwwCgYDVQQKEwNvcmcxFjAUBgNV
|
||||
BAsTDWVsYXN0aWNzZWFyY2gxIDAeBgNVBAMTF0VsYXN0aWNzZWFyY2ggVGVzdCBOb2RlMB4XDTE0
|
||||
MDcyMjA3NTI0N1oXDTE0MTAyMDA3NTI0N1owSDEMMAoGA1UEChMDb3JnMRYwFAYDVQQLEw1lbGFz
|
||||
dGljc2VhcmNoMSAwHgYDVQQDExdFbGFzdGljc2VhcmNoIFRlc3QgTm9kZTCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAJJ7G1e8NzJAhx4PjZQQQimDECwe3nN8ulG23cOE5fYePKh8b0L7
|
||||
cr5O7gMOo7XiXZnptXh8n2J0Fi+3UqnyEallxUmUewdUiwGuzeFHHBtpS2S6LjXR2J+n0Q267u7j
|
||||
wisnUxRv7Cwre/PP2U89b303M1mg/fqNE5Zjb1MpCywoWq+Z07xNDX9aoOWTKQbRuR2931IGZ1QL
|
||||
gX//emymIGGh7XVPoydhtEkPcGXAdFz3OO/K33PTEk24vGzr33dBm/yywa12kaC/6Q49luNmdgsG
|
||||
M5JQ8kKQe0Cv64yMC17pMI2PWwpBuInF1RCx588I3ctHCATm7AL2E/YvG9MqlvkCAwEAAaMhMB8w
|
||||
HQYDVR0OBBYEFPi2gUxc88/DfM89Ku63Z/JkjeQxMA0GCSqGSIb3DQEBCwUAA4IBAQAm3XJyNIAZ
|
||||
jXyI/Hxv0rdZRRIoMzSjA+vyGwcrr4lBLcOxDgtHM68PPfhleHDK4JtTaGftuFqv8ylGa+zVJ+0N
|
||||
e7xFqvFwi0R9goU5j1GgTmt7mYPhgcgS8j9VCTxKZEsO0KmvV5CRz2jB1m4FdHbSR3KKwpgCt6kp
|
||||
K0HifL+pRHQa3Ts9DCBmzG4fwNGPsZvoI47T+JQ9EiUWdEW+dUvr/0T6yLTRsfc0ftZ2OTIJ97JV
|
||||
BgOlAa4b1b8v9933wv53fDwkhhdBvO/uFxrxeYk4szofe2l4fy5CdE7rkDk1mHIHtYWdAQhVL6cL
|
||||
vuDnZppB7koaDhBsssDBpRstnEHL
|
||||
-----END CERTIFICATE-----
|
Binary file not shown.
|
@ -28,4 +28,7 @@ grant {
|
|||
permission java.security.SecurityPermission "getProperty.networkaddress.cache.ttl";
|
||||
permission java.security.SecurityPermission "getProperty.networkaddress.cache.negative.ttl";
|
||||
|
||||
// Needed for accept all ssl certs in tests
|
||||
permission javax.net.ssl.SSLPermission "setHostnameVerifier";
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue