From 2b108203fbed404684a03bce15701405b8dcef78 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 21 Nov 2014 17:02:58 +0100 Subject: [PATCH] SSL/TLS: Added support for different certs per profile In order to run on different certs per port, we needed to adapt the logic of starting up. Also different profiles can now be applied to the N2NAuthenticator, so that a different profile can allow/deny different hosts. In addition minor refactorings have been done * Group keystore/truststore settings instead of using underscores * Change to transport profile settings instead of using specific shield ones Documentation has been updated as well Closes elastic/elasticsearch#290 Original commit: elastic/x-pack-elasticsearch@ad1ab974eafcdc454fd702b141e1d2c92dc9545d --- .../elasticsearch/shield/ssl/SSLService.java | 133 +++++++++++------- .../transport/SecuredTransportModule.java | 6 - .../n2n/IPFilteringN2NAuthenticator.java | 27 ++-- .../transport/n2n/N2NAuthenticator.java | 28 +--- .../transport/n2n/ProfileIpFilterRule.java | 36 +++++ .../netty/N2NNettyUpstreamHandler.java | 10 +- .../NettySecuredHttpServerTransport.java | 13 +- .../netty/NettySecuredTransport.java | 25 +++- .../ActiveDirectoryFactoryTests.java | 4 +- .../shield/authc/ldap/OpenLdapTests.java | 7 +- .../ldap/LdapSslSocketFactoryTests.java | 4 +- .../shield/plugin/ShieldPluginTests.java | 1 - .../shield/ssl/SSLServiceTests.java | 38 +++-- .../shield/test/ShieldIntegrationTest.java | 18 ++- .../n2n/IPFilteringN2NAuthenticatorTests.java | 32 ++++- .../netty/N2NNettyUpstreamHandlerTests.java | 2 +- .../transport/ssl/SslIntegrationTests.java | 6 - .../transport/ssl/SslMultiPortTests.java | 116 +++++++++++++++ .../elasticsearch/test/ShieldRestTests.java | 16 +-- .../simple/testclient-client-profile.cert | 18 +++ .../simple/testclient-client-profile.jks | Bin 0 -> 4051 bytes .../certs/simple/testnode-client-profile.cert | 18 +++ .../certs/simple/testnode-client-profile.jks | Bin 0 -> 3202 bytes .../transport/ssl/certs/simple/testnode.jks | Bin 5818 -> 6757 bytes 24 files changed, 401 insertions(+), 157 deletions(-) create mode 100644 src/main/java/org/elasticsearch/shield/transport/n2n/ProfileIpFilterRule.java create mode 100644 src/test/java/org/elasticsearch/shield/transport/ssl/SslMultiPortTests.java create mode 100644 src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.cert create mode 100644 src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks create mode 100644 src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.cert create mode 100644 src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks diff --git a/src/main/java/org/elasticsearch/shield/ssl/SSLService.java b/src/main/java/org/elasticsearch/shield/ssl/SSLService.java index 038154042d1..0a347847948 100644 --- a/src/main/java/org/elasticsearch/shield/ssl/SSLService.java +++ b/src/main/java/org/elasticsearch/shield/ssl/SSLService.java @@ -23,24 +23,29 @@ import java.util.Arrays; * get SSLEngines and SocketFactories. */ public class SSLService extends AbstractComponent { - static final String[] DEFAULT_CIPHERS = new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" }; + + static final String[] DEFAULT_CIPHERS = new String[]{"TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"}; public static final String SHIELD_TRANSPORT_SSL = "shield.transport.ssl"; public static final String SHIELD_HTTP_SSL = "shield.http.ssl"; public static final String SHIELD_AUTHC_LDAP_URL = "shield.authc.ldap.url"; + + private final TrustManagerFactory trustFactory; private final SSLContext sslContext; private final String[] ciphers; + private final KeyManagerFactory keyManagerFactory; + private final String sslProtocol; @Inject public SSLService(Settings settings) { super(settings); - String keyStorePath = componentSettings.get("keystore", System.getProperty("javax.net.ssl.keyStore")); - String keyStorePassword = componentSettings.get("keystore_password", System.getProperty("javax.net.ssl.keyStorePassword")); - String keyStoreAlgorithm = componentSettings.get("keystore_algorithm", System.getProperty("ssl.KeyManagerFactory.algorithm", KeyManagerFactory.getDefaultAlgorithm())); + String keyStorePath = componentSettings.get("keystore.path", System.getProperty("javax.net.ssl.keyStore")); + String keyStorePassword = componentSettings.get("keystore.password", System.getProperty("javax.net.ssl.keyStorePassword")); + String keyStoreAlgorithm = componentSettings.get("keystore.algorithm", System.getProperty("ssl.KeyManagerFactory.algorithm", KeyManagerFactory.getDefaultAlgorithm())); - String trustStorePath = componentSettings.get("truststore", System.getProperty("javax.net.ssl.trustStore")); - String trustStorePassword = componentSettings.get("truststore_password", System.getProperty("javax.net.ssl.trustStorePassword")); - String trustStoreAlgorithm = componentSettings.get("truststore_algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm())); + String trustStorePath = componentSettings.get("truststore.path", System.getProperty("javax.net.ssl.trustStore")); + String trustStorePassword = componentSettings.get("truststore.password", System.getProperty("javax.net.ssl.trustStorePassword")); + String trustStoreAlgorithm = componentSettings.get("truststore.algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm())); if (trustStorePath == null) { //the keystore will also be the truststore @@ -57,46 +62,14 @@ public class SSLService extends AbstractComponent { this.ciphers = componentSettings.getAsArray("ciphers", DEFAULT_CIPHERS); //protocols supported: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext - String sslProtocol = componentSettings.get("protocol", "TLS"); + this.sslProtocol = componentSettings.get("protocol", "TLS"); logger.debug("using keyStore[{}], keyAlgorithm[{}], trustStore[{}], truststoreAlgorithm[{}], ciphersuites[{}], TLS protocol[{}]", keyStorePath, keyStoreAlgorithm, trustStorePath, trustStoreAlgorithm, ciphers, sslProtocol); - final TrustManagerFactory trustFactory; - try (FileInputStream in = new FileInputStream(trustStorePath)) { - // Load TrustStore - KeyStore trustStore = KeyStore.getInstance("jks"); - trustStore.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray()); - - // Initialize a trust manager factory with the trusted store - trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm); - trustFactory.init(trustStore); - } catch (Exception e) { - throw new ElasticsearchException("Failed to initialize a TrustManagerFactory", e); - } - - KeyStore keyStore; - KeyManagerFactory keyManagerFactory; - try (FileInputStream in = new FileInputStream(keyStorePath)){ - // Load KeyStore - keyStore = KeyStore.getInstance("jks"); - keyStore.load(in, keyStorePassword.toCharArray()); - - // Initialize KeyManagerFactory - keyManagerFactory = KeyManagerFactory.getInstance(keyStoreAlgorithm); - keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()); - - } catch (Exception e) { - throw new ElasticsearchSSLException("Failed to initialize a KeyManagerFactory", e); - } - - // Initialize sslContext - try { - sslContext = SSLContext.getInstance(sslProtocol); - sslContext.init(keyManagerFactory.getKeyManagers(), trustFactory.getTrustManagers(), null); - } catch (Exception e) { - throw new ElasticsearchSSLException("Failed to initialize the SSLContext", e); - } + this.trustFactory = getTrustFactory(trustStorePath, trustStorePassword, trustStoreAlgorithm); + this.keyManagerFactory = createKeyManagerFactory(keyStorePath, keyStorePassword, keyStoreAlgorithm); + this.sslContext = createSslContext(trustFactory); } /** @@ -110,26 +83,86 @@ public class SSLService extends AbstractComponent { * This engine is configured with a trust manager and a keystore that should have only one private key. * Four possible usages for elasticsearch exist: * Node-to-Node outbound: - * - sslEngine.setUseClientMode(true) + * - sslEngine.setUseClientMode(true) * Node-to-Node inbound: - * - sslEngine.setUseClientMode(false) - * - sslEngine.setNeedClientAuth(true) + * - sslEngine.setUseClientMode(false) + * - sslEngine.setNeedClientAuth(true) * Client-to-Node: - * - sslEngine.setUseClientMode(true) + * - sslEngine.setUseClientMode(true) * Http Client-to-Node (inbound): - * - sslEngine.setUserClientMode(false) - * - sslEngine.setNeedClientAuth(false) + * - sslEngine.setUserClientMode(false) + * - sslEngine.setNeedClientAuth(false) */ public SSLEngine createSSLEngine() { + return createSSLEngine(this.sslContext); + } + + public SSLEngine createSSLEngineWithTruststore(Settings settings) { + String trustStore = settings.get("truststore.path", System.getProperty("javax.net.ssl.trustStore")); + String trustStorePassword = settings.get("truststore.password", System.getProperty("javax.net.ssl.trustStorePassword")); + String trustStoreAlgorithm = settings.get("truststore.algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm())); + + // no truststore or password, return regular ssl engine + if (trustStore == null || trustStorePassword == null) { + return createSSLEngine(); + } + + TrustManagerFactory trustFactory = getTrustFactory(trustStore, trustStorePassword, trustStoreAlgorithm); + SSLContext customTruststoreSSLContext = createSslContext(trustFactory); + return createSSLEngine(customTruststoreSSLContext); + } + + private SSLEngine createSSLEngine(SSLContext sslContext) { SSLEngine sslEngine = sslContext.createSSLEngine(); try { sslEngine.setEnabledCipherSuites(ciphers); } catch (Throwable t) { - throw new ElasticsearchSSLException("Error loading cipher suites ["+ Arrays.asList(ciphers)+"]", t); + throw new ElasticsearchSSLException("Error loading cipher suites [" + Arrays.asList(ciphers) + "]", t); } return sslEngine; } + private KeyManagerFactory createKeyManagerFactory(String keyStore, String keyStorePassword, String keyStoreAlgorithm) { + try (FileInputStream in = new FileInputStream(keyStore)) { + // Load KeyStore + KeyStore ks = KeyStore.getInstance("jks"); + ks.load(in, keyStorePassword.toCharArray()); + + // Initialize KeyManagerFactory + KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm); + kmf.init(ks, keyStorePassword.toCharArray()); + return kmf; + } catch (Exception e) { + throw new ElasticsearchSSLException("Failed to initialize a KeyManagerFactory", e); + } + } + + private SSLContext createSslContext(TrustManagerFactory trustFactory) { + // Initialize sslContext + try { + SSLContext sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init(keyManagerFactory.getKeyManagers(), trustFactory.getTrustManagers(), null); + return sslContext; + } catch (Exception e) { + throw new ElasticsearchSSLException("Failed to initialize the SSLContext", e); + } + } + + private TrustManagerFactory getTrustFactory(String trustStore, String trustStorePassword, String trustStoreAlgorithm) { + try (FileInputStream in = new FileInputStream(trustStore)) { + // Load TrustStore + KeyStore ks = KeyStore.getInstance("jks"); + ks.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray()); + + // Initialize a trust manager factory with the trusted store + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm); + trustFactory.init(ks); + return trustFactory; + } catch (Exception e) { + throw new ElasticsearchException("Failed to initialize a TrustManagerFactory", e); + } + } + public static boolean isSSLEnabled(Settings settings) { return settings.getAsBoolean(SHIELD_TRANSPORT_SSL, false) || settings.getAsBoolean(SHIELD_HTTP_SSL, false) || diff --git a/src/main/java/org/elasticsearch/shield/transport/SecuredTransportModule.java b/src/main/java/org/elasticsearch/shield/transport/SecuredTransportModule.java index 15eed1e48e1..017d0ea8993 100644 --- a/src/main/java/org/elasticsearch/shield/transport/SecuredTransportModule.java +++ b/src/main/java/org/elasticsearch/shield/transport/SecuredTransportModule.java @@ -8,13 +8,11 @@ package org.elasticsearch.shield.transport; import org.elasticsearch.common.collect.ImmutableList; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.PreProcessModule; -import org.elasticsearch.common.inject.util.Providers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.SecurityFilter; import org.elasticsearch.shield.plugin.ShieldPlugin; import org.elasticsearch.shield.support.AbstractShieldModule; import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator; -import org.elasticsearch.shield.transport.netty.N2NNettyUpstreamHandler; import org.elasticsearch.shield.transport.netty.NettySecuredHttpServerTransportModule; import org.elasticsearch.shield.transport.netty.NettySecuredTransportModule; import org.elasticsearch.transport.TransportModule; @@ -52,7 +50,6 @@ public class SecuredTransportModule extends AbstractShieldModule.Spawn implement if (clientMode) { // no ip filtering on the client - bind(N2NNettyUpstreamHandler.class).toProvider(Providers.of(null)); bind(ServerTransportFilter.class).toInstance(ServerTransportFilter.NOOP); return; } @@ -60,9 +57,6 @@ public class SecuredTransportModule extends AbstractShieldModule.Spawn implement bind(ServerTransportFilter.class).to(SecurityFilter.ServerTransport.class).asEagerSingleton(); if (settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true)) { bind(IPFilteringN2NAuthenticator.class).asEagerSingleton(); - bind(N2NNettyUpstreamHandler.class).asEagerSingleton(); - } else { - bind(N2NNettyUpstreamHandler.class).toProvider(Providers.of(null)); } } } diff --git a/src/main/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticator.java b/src/main/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticator.java index 19f1408c267..5da09bead09 100644 --- a/src/main/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticator.java +++ b/src/main/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticator.java @@ -41,11 +41,11 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 private static final Pattern COMMA_DELIM = Pattern.compile("\\s*,\\s*"); private static final String DEFAULT_FILE = "ip_filter.yml"; - private static final IpFilterRule[] NO_RULES = new IpFilterRule[0]; + private static final ProfileIpFilterRule[] NO_RULES = new ProfileIpFilterRule[0]; private final Path file; - private volatile IpFilterRule[] rules = NO_RULES; + private volatile ProfileIpFilterRule[] rules = NO_RULES; @Inject public IPFilteringN2NAuthenticator(Settings settings, Environment env, ResourceWatcherService watcherService) { @@ -65,12 +65,12 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 return Paths.get(location); } - public static IpFilterRule[] parseFile(Path path, ESLogger logger) { + public static ProfileIpFilterRule[] parseFile(Path path, ESLogger logger) { if (!Files.exists(path)) { return NO_RULES; } - List rules = new ArrayList<>(); + List rules = new ArrayList<>(); try (XContentParser parser = YamlXContent.yamlXContent.createParser(Files.newInputStream(path))) { XContentParser.Token token; @@ -78,8 +78,8 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT && token != null) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); - if (!"allow".equals(currentFieldName) && !"deny".equals(currentFieldName)) { - throw new ElasticsearchParseException("Field name [" + currentFieldName + "] not valid. Must be [allow] or [deny]"); + if (!currentFieldName.endsWith("allow") && !currentFieldName.endsWith("deny")) { + throw new ElasticsearchParseException("Field name [" + currentFieldName + "] not valid. Must be [allow] or [deny] or using a profile"); } } else if (token == XContentParser.Token.VALUE_STRING && currentFieldName != null) { String value = parser.text(); @@ -87,14 +87,15 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 throw new ElasticsearchParseException("Field value for fieldname [" + currentFieldName + "] must not be empty"); } - boolean isAllowRule = currentFieldName.equals("allow"); + boolean isAllowRule = currentFieldName.endsWith("allow"); + String profile = currentFieldName.contains(".") ? currentFieldName.substring(0, currentFieldName.indexOf(".")) : "default"; if (value.contains(",")) { for (String rule : COMMA_DELIM.split(parser.text().trim())) { - rules.add(getRule(isAllowRule, rule)); + rules.add(new ProfileIpFilterRule(profile, getRule(isAllowRule, rule))); } } else { - rules.add(getRule(isAllowRule, value)); + rules.add(new ProfileIpFilterRule(profile, getRule(isAllowRule, value))); } } @@ -108,7 +109,7 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 } logger.debug("Loaded {} ip filtering rules", rules.size()); - return rules.toArray(new IpFilterRule[rules.size()]); + return rules.toArray(new ProfileIpFilterRule[rules.size()]); } private static IpFilterRule getRule(boolean isAllowRule, String value) throws UnknownHostException { @@ -124,9 +125,9 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2 } @Override - public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) { - for (IpFilterRule rule : rules) { - if (rule.contains(peerAddress)) { + public boolean authenticate(@Nullable Principal peerPrincipal, String profile, InetAddress peerAddress, int peerPort) { + for (ProfileIpFilterRule rule : rules) { + if (rule.contains(profile, peerAddress)) { boolean isAllowed = rule.isAllowRule(); logger.trace("Authentication rule matched for host [{}]: {}", peerAddress, isAllowed); return isAllowed; diff --git a/src/main/java/org/elasticsearch/shield/transport/n2n/N2NAuthenticator.java b/src/main/java/org/elasticsearch/shield/transport/n2n/N2NAuthenticator.java index 27b50ed8d08..c12d9d67c83 100644 --- a/src/main/java/org/elasticsearch/shield/transport/n2n/N2NAuthenticator.java +++ b/src/main/java/org/elasticsearch/shield/transport/n2n/N2NAuthenticator.java @@ -15,32 +15,6 @@ import java.security.Principal; */ public interface N2NAuthenticator { - N2NAuthenticator NO_AUTH = new N2NAuthenticator() { - @Override - public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) { - return true; - } - }; + boolean authenticate(@Nullable Principal peerPrincipal, @Nullable String profile, InetAddress peerAddress, int peerPort); - boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort); - - - class Compound implements N2NAuthenticator { - - private N2NAuthenticator[] authenticators; - - public Compound(N2NAuthenticator... authenticators) { - this.authenticators = authenticators; - } - - @Override - public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) { - for (int i = 0; i < authenticators.length; i++) { - if (!authenticators[i].authenticate(peerPrincipal, peerAddress, peerPort)) { - return false; - } - } - return true; - } - } } diff --git a/src/main/java/org/elasticsearch/shield/transport/n2n/ProfileIpFilterRule.java b/src/main/java/org/elasticsearch/shield/transport/n2n/ProfileIpFilterRule.java new file mode 100644 index 00000000000..8b9cde1030b --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/transport/n2n/ProfileIpFilterRule.java @@ -0,0 +1,36 @@ +/* + * 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.transport.n2n; + +import org.elasticsearch.common.netty.handler.ipfilter.IpFilterRule; + +import java.net.InetAddress; + +/** + * helper interface for filter rules, which takes a tcp transport profile into account + */ +public class ProfileIpFilterRule { + + private final String profile; + private final IpFilterRule ipFilterRule; + + public ProfileIpFilterRule(String profile, IpFilterRule ipFilterRule) { + this.profile = profile; + this.ipFilterRule = ipFilterRule; + } + + public boolean contains(String profile, InetAddress inetAddress) { + return this.profile.equals(profile) && ipFilterRule.contains(inetAddress); + } + + public boolean isAllowRule() { + return ipFilterRule.isAllowRule(); + } + + public boolean isDenyRule() { + return ipFilterRule.isDenyRule(); + } +} diff --git a/src/main/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandler.java b/src/main/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandler.java index 20f4e8059f1..2ad50dc8691 100644 --- a/src/main/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandler.java +++ b/src/main/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandler.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.shield.transport.netty; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.netty.channel.ChannelEvent; import org.elasticsearch.common.netty.channel.ChannelHandler; import org.elasticsearch.common.netty.channel.ChannelHandlerContext; @@ -20,17 +19,18 @@ import java.net.InetSocketAddress; @ChannelHandler.Sharable public class N2NNettyUpstreamHandler extends IpFilteringHandlerImpl { - private IPFilteringN2NAuthenticator authenticator; + private final IPFilteringN2NAuthenticator authenticator; + private final String profile; - @Inject - public N2NNettyUpstreamHandler(IPFilteringN2NAuthenticator authenticator) { + public N2NNettyUpstreamHandler(IPFilteringN2NAuthenticator authenticator, String profile) { this.authenticator = authenticator; + this.profile = profile; } @Override protected boolean accept(ChannelHandlerContext channelHandlerContext, ChannelEvent channelEvent, InetSocketAddress inetSocketAddress) throws Exception { // at this stage no auth has happened, so we do not have any principal anyway - return authenticator.authenticate(null, inetSocketAddress.getAddress(), inetSocketAddress.getPort()); + return authenticator.authenticate(null, profile, inetSocketAddress.getAddress(), inetSocketAddress.getPort()); } } diff --git a/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredHttpServerTransport.java b/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredHttpServerTransport.java index d40527e06a8..2cb1bbf054c 100644 --- a/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredHttpServerTransport.java +++ b/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredHttpServerTransport.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.http.netty.NettyHttpServerTransport; import org.elasticsearch.shield.ssl.SSLService; +import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator; import javax.net.ssl.SSLEngine; @@ -24,17 +25,19 @@ import javax.net.ssl.SSLEngine; public class NettySecuredHttpServerTransport extends NettyHttpServerTransport { private final boolean ssl; - private final N2NNettyUpstreamHandler shieldUpstreamHandler; + private final boolean ipFilterEnabled; + private final IPFilteringN2NAuthenticator authenticator; private final SSLService sslService; @Inject public NettySecuredHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, - @Nullable N2NNettyUpstreamHandler shieldUpstreamHandler, @Nullable SSLService sslService) { + @Nullable IPFilteringN2NAuthenticator authenticator, @Nullable SSLService sslService) { super(settings, networkService, bigArrays); + this.authenticator = authenticator; this.ssl = settings.getAsBoolean("shield.http.ssl", false); this.sslService = sslService; - this.shieldUpstreamHandler = shieldUpstreamHandler; assert !ssl || sslService != null : "ssl is enabled yet the ssl service is null"; + this.ipFilterEnabled = settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true); } @Override @@ -58,8 +61,8 @@ public class NettySecuredHttpServerTransport extends NettyHttpServerTransport { pipeline.addFirst("ssl", new SslHandler(engine)); } - if (shieldUpstreamHandler != null) { - pipeline.addFirst("ipfilter", shieldUpstreamHandler); + if (ipFilterEnabled) { + pipeline.addFirst("ipfilter", new N2NNettyUpstreamHandler(authenticator, "default")); } return pipeline; } diff --git a/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredTransport.java b/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredTransport.java index 589c3b63e41..08fc8958044 100644 --- a/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredTransport.java +++ b/src/main/java/org/elasticsearch/shield/transport/netty/NettySecuredTransport.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.shield.ssl.SSLService; +import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.netty.NettyTransport; @@ -26,16 +27,18 @@ import javax.net.ssl.SSLEngine; public class NettySecuredTransport extends NettyTransport { private final boolean ssl; - private final N2NNettyUpstreamHandler shieldUpstreamHandler; private final SSLService sslService; + private final boolean ipFilterEnabled; + private final IPFilteringN2NAuthenticator authenticator; @Inject public NettySecuredTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version, - @Nullable N2NNettyUpstreamHandler shieldUpstreamHandler, @Nullable SSLService sslService) { + @Nullable IPFilteringN2NAuthenticator authenticator, @Nullable SSLService sslService) { super(settings, threadPool, networkService, bigArrays, version); - this.shieldUpstreamHandler = shieldUpstreamHandler; - this.ssl = settings.getAsBoolean("shield.transport.ssl", false); + this.authenticator = authenticator; + this.ipFilterEnabled = settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true); this.sslService = sslService; + this.ssl = settings.getAsBoolean("shield.transport.ssl", false); assert !ssl || sslService != null : "ssl is enabled yet the ssl service is null"; } @@ -51,23 +54,31 @@ public class NettySecuredTransport extends NettyTransport { private class SslServerChannelPipelineFactory extends ServerChannelPipelineFactory { + private final Settings profileSettings; + public SslServerChannelPipelineFactory(NettyTransport nettyTransport, String name, Settings settings, Settings profileSettings) { super(nettyTransport, name, settings); + this.profileSettings = profileSettings; } @Override public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = super.getPipeline(); if (ssl) { - SSLEngine serverEngine = sslService.createSSLEngine(); + SSLEngine serverEngine; + if (profileSettings.get("shield.truststore.path") != null) { + serverEngine = sslService.createSSLEngineWithTruststore(profileSettings.getByPrefix("shield.")); + } else { + serverEngine = sslService.createSSLEngine(); + } serverEngine.setUseClientMode(false); serverEngine.setNeedClientAuth(true); pipeline.addFirst("ssl", new SslHandler(serverEngine)); pipeline.replace("dispatcher", "dispatcher", new SecuredMessageChannelHandler(nettyTransport, logger)); } - if (shieldUpstreamHandler != null) { - pipeline.addFirst("ipfilter", shieldUpstreamHandler); + if (ipFilterEnabled) { + pipeline.addFirst("ipfilter", new N2NNettyUpstreamHandler(authenticator, name)); } return pipeline; } diff --git a/src/test/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryFactoryTests.java index b4f67e5f938..0acec428782 100644 --- a/src/test/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryFactoryTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryFactoryTests.java @@ -37,8 +37,8 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase { public static void setTrustStore() throws URISyntaxException { File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile(); LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder() - .put("shield.ssl.keystore", filename) - .put("shield.ssl.keystore_password", "changeit") + .put("shield.ssl.keystore.path", filename) + .put("shield.ssl.keystore.password", "changeit") .build())); } diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java index b8f19d043e4..bbbd173e782 100644 --- a/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java @@ -7,8 +7,8 @@ package org.elasticsearch.shield.authc.ldap; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.shield.authc.support.SecuredStringTests; -import org.elasticsearch.shield.ssl.SSLService; import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory; +import org.elasticsearch.shield.ssl.SSLService; import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.junit.annotations.Network; import org.junit.AfterClass; @@ -25,14 +25,13 @@ import static org.hamcrest.Matchers.hasItem; public class OpenLdapTests extends ElasticsearchTestCase { public static final String OPEN_LDAP_URL = "ldaps://54.200.235.244:636"; public static final String PASSWORD = "NickFuryHeartsES"; - public static final String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.'; @BeforeClass public static void setTrustStore() throws URISyntaxException { File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile(); LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder() - .put("shield.ssl.keystore", filename) - .put("shield.ssl.keystore_password", "changeit") + .put("shield.ssl.keystore.path", filename) + .put("shield.ssl.keystore.password", "changeit") .build())); } diff --git a/src/test/java/org/elasticsearch/shield/authc/support/ldap/LdapSslSocketFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/support/ldap/LdapSslSocketFactoryTests.java index a1162c233b4..7bbe1f354f9 100644 --- a/src/test/java/org/elasticsearch/shield/authc/support/ldap/LdapSslSocketFactoryTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/support/ldap/LdapSslSocketFactoryTests.java @@ -28,8 +28,8 @@ public class LdapSslSocketFactoryTests extends ElasticsearchTestCase { public static void setTrustStore() throws URISyntaxException { File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile(); LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder() - .put("shield.ssl.keystore", filename) - .put("shield.ssl.keystore_password", "changeit") + .put("shield.ssl.keystore.path", filename) + .put("shield.ssl.keystore.password", "changeit") .build())); } diff --git a/src/test/java/org/elasticsearch/shield/plugin/ShieldPluginTests.java b/src/test/java/org/elasticsearch/shield/plugin/ShieldPluginTests.java index f1041f6b3d9..8b10d350c74 100644 --- a/src/test/java/org/elasticsearch/shield/plugin/ShieldPluginTests.java +++ b/src/test/java/org/elasticsearch/shield/plugin/ShieldPluginTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.node.internal.InternalNode; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.test.ShieldIntegrationTest; -import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; import org.elasticsearch.test.rest.client.http.HttpResponse; import org.junit.Test; diff --git a/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java b/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java index 5970495c2ce..7ff37c4b624 100644 --- a/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java +++ b/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java @@ -5,17 +5,18 @@ */ package org.elasticsearch.shield.ssl; +import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ElasticsearchTestCase; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import javax.net.ssl.SSLEngine; import java.io.File; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; public class SSLServiceTests extends ElasticsearchTestCase { @@ -30,10 +31,10 @@ public class SSLServiceTests extends ElasticsearchTestCase { public void testThatInvalidProtocolThrowsException() throws Exception { new SSLService(settingsBuilder() .put("shield.ssl.protocol", "non-existing") - .put("shield.ssl.keystore", testnodeStore.getPath()) - .put("shield.ssl.keystore_password", "testnode") - .put("shield.ssl.truststore", testnodeStore.getPath()) - .put("shield.ssl.truststore_password", "testnode") + .put("shield.ssl.keystore.path", testnodeStore.getPath()) + .put("shield.ssl.keystore.password", "testnode") + .put("shield.ssl.truststore.path", testnodeStore.getPath()) + .put("shield.ssl.truststore.password", "testnode") .build()); } @@ -41,10 +42,10 @@ public class SSLServiceTests extends ElasticsearchTestCase { public void testSpecificProtocol() { SSLService ssl = new SSLService(settingsBuilder() .put("shield.ssl.protocol", "TLSv1.2") - .put("shield.ssl.keystore", testnodeStore.getPath()) - .put("shield.ssl.keystore_password", "testnode") - .put("shield.ssl.truststore", testnodeStore.getPath()) - .put("shield.ssl.truststore_password", "testnode") + .put("shield.ssl.keystore.path", testnodeStore.getPath()) + .put("shield.ssl.keystore.password", "testnode") + .put("shield.ssl.truststore.path", testnodeStore.getPath()) + .put("shield.ssl.truststore.password", "testnode") .build()); assertThat(ssl.createSSLEngine().getSSLParameters().getProtocols(), arrayContaining("TLSv1.2")); } @@ -111,6 +112,23 @@ public class SSLServiceTests extends ElasticsearchTestCase { assertSSLEnabled(settings); } + @Test + public void testThatCustomTruststoreCanBeSpecified() throws Exception { + File testClientStore = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks").toURI()); + + SSLService sslService = new SSLService(settingsBuilder() + .put("shield.ssl.keystore.path", testnodeStore.getPath()) + .put("shield.ssl.keystore.password", "testnode") + .build()); + + ImmutableSettings.Builder settingsBuilder = settingsBuilder() + .put("truststore.path", testClientStore.getPath()) + .put("truststore.password", "testclient"); + + SSLEngine sslEngineWithTruststore = sslService.createSSLEngineWithTruststore(settingsBuilder.build()); + assertThat(sslEngineWithTruststore, is(not(nullValue()))); + } + private void assertSSLEnabled(Settings settings) { assertThat(SSLService.isSSLEnabled(settings), is(true)); } diff --git a/src/test/java/org/elasticsearch/shield/test/ShieldIntegrationTest.java b/src/test/java/org/elasticsearch/shield/test/ShieldIntegrationTest.java index d6ba70f7ea5..adf1256a120 100644 --- a/src/test/java/org/elasticsearch/shield/test/ShieldIntegrationTest.java +++ b/src/test/java/org/elasticsearch/shield/test/ShieldIntegrationTest.java @@ -9,6 +9,9 @@ import com.google.common.base.Charsets; import com.google.common.net.InetAddresses; import org.apache.lucene.util.AbstractRandomizedTest; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; +import org.elasticsearch.client.Client; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.os.OsUtils; import org.elasticsearch.common.settings.ImmutableSettings; @@ -34,6 +37,7 @@ import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilde import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue; 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.instanceOf; import static org.hamcrest.Matchers.is; @@ -158,13 +162,13 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest ImmutableSettings.Builder builder = settingsBuilder() .put("shield.transport.ssl", true) - .put("shield.ssl.keystore", store.getPath()) - .put("shield.ssl.keystore_password", password) + .put("shield.ssl.keystore.path", store.getPath()) + .put("shield.ssl.keystore.password", password) .put("shield.http.ssl", true); if (randomBoolean()) { - builder.put("shield.ssl.truststore", store.getPath()) - .put("shield.ssl.truststore_password", password); + builder.put("shield.ssl.truststore.path", store.getPath()) + .put("shield.ssl.truststore.password", password); } return builder.build(); @@ -179,4 +183,10 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest return null; } } + + protected void assertGreenClusterState(Client client) { + ClusterHealthResponse clusterHealthResponse = client.admin().cluster().prepareHealth().get(); + assertNoTimeout(clusterHealthResponse); + assertThat(clusterHealthResponse.getStatus(), is(ClusterHealthStatus.GREEN)); + } } diff --git a/src/test/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticatorTests.java b/src/test/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticatorTests.java index bb39e0a05aa..16d495fc033 100644 --- a/src/test/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticatorTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/n2n/IPFilteringN2NAuthenticatorTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.junit.annotations.Network; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.junit.After; @@ -141,6 +142,17 @@ public class IPFilteringN2NAuthenticatorTests extends ElasticsearchTestCase { assertAddressIsAllowed("127.0.0.1"); } + @Test + @TestLogging("_root:TRACE") + public void testThatProfilesAreSupported() throws Exception { + writeConfigFile("allow: localhost\ndeny: all\nclient.allow: 192.168.0.1\nclient.deny: all"); + + assertAddressIsAllowed("127.0.0.1"); + assertAddressIsDenied("192.168.0.1"); + assertAddressIsAllowedForProfile("client", "192.168.0.1"); + assertAddressIsDeniedForProfile("client", "192.168.0.2"); + } + @Test(expected = ElasticsearchParseException.class) public void testThatInvalidFileThrowsCorrectException() throws Exception { writeConfigFile("deny: all allow: all \n\n"); @@ -155,17 +167,25 @@ public class IPFilteringN2NAuthenticatorTests extends ElasticsearchTestCase { ipFilteringN2NAuthenticator = new IPFilteringN2NAuthenticator(settings, new Environment(), resourceWatcherService); } - private void assertAddressIsAllowed(String ... inetAddresses) { + private void assertAddressIsAllowedForProfile(String profile, String ... inetAddresses) { for (String inetAddress : inetAddresses) { String message = String.format(Locale.ROOT, "Expected address %s to be allowed", inetAddress); - assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, InetAddresses.forString(inetAddress), 1024), is(true)); + assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, profile, InetAddresses.forString(inetAddress), 1024), is(true)); + } + } + + private void assertAddressIsAllowed(String ... inetAddresses) { + assertAddressIsAllowedForProfile("default", inetAddresses); + } + + private void assertAddressIsDeniedForProfile(String profile, String ... inetAddresses) { + for (String inetAddress : inetAddresses) { + String message = String.format(Locale.ROOT, "Expected address %s to be denied", inetAddress); + assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, profile, InetAddresses.forString(inetAddress), 1024), is(false)); } } private void assertAddressIsDenied(String ... inetAddresses) { - for (String inetAddress : inetAddresses) { - String message = String.format(Locale.ROOT, "Expected address %s to be denied", inetAddress); - assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, InetAddresses.forString(inetAddress), 1024), is(false)); - } + assertAddressIsDeniedForProfile("default", inetAddresses); } } diff --git a/src/test/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandlerTests.java b/src/test/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandlerTests.java index 831ca27c631..bd679428bd6 100644 --- a/src/test/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandlerTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/netty/N2NNettyUpstreamHandlerTests.java @@ -47,7 +47,7 @@ public class N2NNettyUpstreamHandlerTests extends ElasticsearchTestCase { Settings settings = settingsBuilder().put("shield.transport.n2n.ip_filter.file", configFile.getPath()).build(); IPFilteringN2NAuthenticator ipFilteringN2NAuthenticator = new IPFilteringN2NAuthenticator(settings, new Environment(), resourceWatcherService); - nettyUpstreamHandler = new N2NNettyUpstreamHandler(ipFilteringN2NAuthenticator); + nettyUpstreamHandler = new N2NNettyUpstreamHandler(ipFilteringN2NAuthenticator, "default"); } @After diff --git a/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java b/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java index ceb6c2aee4c..2016ff3e766 100644 --- a/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java @@ -181,10 +181,4 @@ public class SslIntegrationTests extends ShieldIntegrationTest { String data = Streams.copyToString(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8)); assertThat(data, containsString("You Know, for Search")); } - - private void assertGreenClusterState(Client client) { - ClusterHealthResponse clusterHealthResponse = client.admin().cluster().prepareHealth().get(); - assertNoTimeout(clusterHealthResponse); - assertThat(clusterHealthResponse.getStatus(), is(ClusterHealthStatus.GREEN)); - } } diff --git a/src/test/java/org/elasticsearch/shield/transport/ssl/SslMultiPortTests.java b/src/test/java/org/elasticsearch/shield/transport/ssl/SslMultiPortTests.java new file mode 100644 index 00000000000..3fe16d0fac7 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/transport/ssl/SslMultiPortTests.java @@ -0,0 +1,116 @@ +/* + * 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.transport.ssl; + +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.client.transport.TransportClient; +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.plugins.PluginsService; +import org.elasticsearch.shield.test.ShieldIntegrationTest; +import org.elasticsearch.shield.transport.netty.NettySecuredTransport; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportModule; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; + +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.hamcrest.Matchers.is; + +/** + * + */ +public class SslMultiPortTests extends ShieldIntegrationTest { + + private ImmutableSettings.Builder builder; + private static int randomClientPort; + + @BeforeClass + public static void getRandomPort() { + randomClientPort = randomIntBetween(49000, 65500); // ephemeral port + } + + @Before + public void setupBuilder() { + builder = settingsBuilder() + .put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName()) + .put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false) + .put("node.mode", "network") + .put("cluster.name", internalCluster().getClusterName()); + + setUser(builder); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + String randomClientPortRange = randomClientPort + "-" + (randomClientPort+100); + + File store; + try { + store = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks").toURI()); + assertThat(store.exists(), is(true)); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return settingsBuilder() + .put(super.nodeSettings(nodeOrdinal)) + // settings for default key profile + .put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode.jks", "testnode")) + // client set up here + .put("transport.profiles.client.port", randomClientPortRange) + .put("transport.profiles.client.bind_host", "localhost") // make sure this is "localhost", no matter if ipv4 or ipv6, but be consistent + .put("transport.profiles.client.shield.truststore.path", store.getAbsolutePath()) // settings for client truststore + .put("transport.profiles.client.shield.truststore.password", "testnode-client-profile") + .put("shield.audit.enabled", false ) + + .build(); + } + + @Test + public void testThatStandardTransportClientCanConnectToDefaultProfile() throws Exception { + builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks", "testclient")); + try (TransportClient transportClient = new TransportClient(builder, false)) { + TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress(); + transportClient.addTransportAddress(transportAddress); + assertGreenClusterState(transportClient); + + } + } + + @Test(expected = NoNodeAvailableException.class) + public void testThatStandardTransportClientCannotConnectToClientProfile() throws Exception { + builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks", "testclient")); + try (TransportClient transportClient = new TransportClient(builder, false)) { + transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", randomClientPort)); + transportClient.admin().cluster().prepareHealth().get(); + } + } + + @Test + public void testThatProfileTransportClientCanConnectToClientProfile() throws Exception { + builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile")); + try (TransportClient transportClient = new TransportClient(builder, false)) { + transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", randomClientPort)); + assertGreenClusterState(transportClient); + } + } + + @Test(expected = NoNodeAvailableException.class) + public void testThatProfileTransportClientCannotConnectToDefaultProfile() throws Exception { + builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile")); + try (TransportClient transportClient = new TransportClient(builder, false)) { + TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress(); + transportClient.addTransportAddress(transportAddress); + transportClient.admin().cluster().prepareHealth().get(); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/ShieldRestTests.java b/src/test/java/org/elasticsearch/test/ShieldRestTests.java index 7a0dc3d88ef..cf4a560562f 100644 --- a/src/test/java/org/elasticsearch/test/ShieldRestTests.java +++ b/src/test/java/org/elasticsearch/test/ShieldRestTests.java @@ -109,10 +109,10 @@ public class ShieldRestTests extends ElasticsearchRestTests { .put("shield.authz.store.files.roles", createFile(folder, "roles.yml", CONFIG_ROLE_ALLOW_ALL)) .put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL)) .put("shield.transport.ssl", ENABLE_TRANSPORT_SSL) - .put("shield.ssl.keystore", store.getPath()) - .put("shield.ssl.keystore_password", password) - .put("shield.ssl.truststore", store.getPath()) - .put("shield.ssl.truststore_password", password) + .put("shield.ssl.keystore.path", store.getPath()) + .put("shield.ssl.keystore.password", password) + .put("shield.ssl.truststore.path", store.getPath()) + .put("shield.ssl.truststore.password", password) .put("shield.http.ssl", false) .put("transport.tcp.port", BASE_PORT_RANGE) .putArray("discovery.zen.ping.unicast.hosts", "127.0.0.1:" + BASE_PORT, "127.0.0.1:" + (BASE_PORT + 1), "127.0.0.1:" + (BASE_PORT + 2), "127.0.0.1:" + (BASE_PORT + 3)) @@ -146,10 +146,10 @@ public class ShieldRestTests extends ElasticsearchRestTests { .put("node.mode", "network") .put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL)) .put("shield.transport.ssl", ENABLE_TRANSPORT_SSL) - .put("shield.ssl.keystore", store.getPath()) - .put("shield.ssl.keystore_password", password) - .put("shield.ssl.truststore", store.getPath()) - .put("shield.ssl.truststore_password", password) + .put("shield.ssl.keystore.path", store.getPath()) + .put("shield.ssl.keystore.password", password) + .put("shield.ssl.truststore.path", store.getPath()) + .put("shield.ssl.truststore.password", password) .put("cluster.name", internalCluster().getClusterName()) .build(); } diff --git a/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.cert b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.cert new file mode 100644 index 00000000000..25c05c410b8 --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEWHpBBTANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAw +DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD +VQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MTEwNTEwMzMxMVoXDTE1MDIwMzEw +MzMxMVowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5r +bm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93 +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANMk6NoZl5kbrt9ycsCAms/aivFvmd17 +OTwNPqVvsEa7/uCdaDAiYvUtdjs8LMh7uN5s/6DuimpmbKh/XmT9wljWGpT/zPQQhhontvxefXCr +Gp6z8Bs6z/xrbN8GU+M6D4AFOOnZ8YdlXEmFrCCdp7Nu6eqEUa5ui/1HLrTvey0xN2geZHwfHyPw +ZY9eRlLn2v9lzilzrd3H8AIJ4vBBUx44/CP90ocYUSArLOm8TFvRSOfp/vqz0j6gnGOebN9e4a0B +gAZVQmN8g+SrJsRNGgjGgLj7AjQxh9iThpWIWNeJ2UTeuswvANvxna0zRcr6fejmtaAYO9SST76c +oaZSvS0CAwEAAaMhMB8wHQYDVR0OBBYEFJnOrwiIIEz8XMVIXL8g3QRgtUafMA0GCSqGSIb3DQEB +CwUAA4IBAQCYQW+1efFngQbxDs1jZp+rBAeQ2rQFc4arWx4HOCaRyjlPCwNpjwlN3JM+OtqqR4Z/ +1HMRpPjgdayiTQ3HsVRWzMVm4NCVHx5LmahMCHv+1mru4Ut7BbbupgAlsQ3vtKcgKIdTVhO8vJlP +IYprm388k06/t3CuQJSaCNxElpe3kIldXMeFRKi2TcqOXjb/Nw2L+gZz/+XJLWLOmoAy+2Pq629f +bk1d/lG3pf0QV/X1kcMiHV320iI/CZJWvwuJK73Ukg1RG8CTYt649R1Trqns6bimFyOMMnBlNtD5 +PJiZ0+528XTNbA6Vhz/bXN2g7lDtedy6xJSO1ULFFE9/iUKX +-----END CERTIFICATE----- diff --git a/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks new file mode 100644 index 0000000000000000000000000000000000000000..bcf510bddff045bf533f9588a18ad5a5cffdc1e5 GIT binary patch literal 4051 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 GhyDQ&Ad5!; literal 0 HcmV?d00001 diff --git a/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.cert b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.cert new file mode 100644 index 00000000000..30ae6c12519 --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEHucrFzANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAw +DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD +VQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MTEwNDE3MTUxNVoXDTE1MDIwMjE3 +MTUxNVowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5r +bm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93 +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKrBa2XNWQUk8+TdwG1ZSiwGfQOKNQko +JoX7Cx977L3RQIEs9Q2JxsSRM3wh4uBZzrZ/NCnxKOtw3bKC6B9dUJLXXwZFc7YTtNfcZr8S2000 +UW6mre/4u54wnkJD/ahuoZ/FCUE7ETB+Jeg3uDhyoUCcySo96OPvZpy/ctXTmkDuai3h+0NvUgpY +yll2LcWBaottW0X6YveCwF78CNDVCSLRjGKqLa3QWFGFMQWdmYrzIaCb1e4U0/8WHM0ylw/vYuvz +u+BDXOsoIPUn4eDdeWtxXA/ZGhjDCfTb0GWSSBfmciMY6yIBpQalt/yRQ/2AL9t+G2fc0th2FzD6 +2UZOexMCAwEAAaMhMB8wHQYDVR0OBBYEFEL891TiYG3R/E5kGjxVY+SwAY5BMA0GCSqGSIb3DQEB +CwUAA4IBAQB372cFMKkLlnH2JbMMtVooXWjF40TJdUOU/ImB+i7rLdVUFX/HmexiL3nDziwOMhTH +N3iEDlxcBeVP+XxZouStwAZP0MmezoGiEjRG53w8gBHSkiWmkKBHYZe1JedeZxEWQCCI0zMh14PY +j5kmgD/sFSvWJCP8UpJnSKTj4ZKAiITmsgSJL/0A5R2Af2lTD5k5sQyvNt1im6atuKdnO96Iqthr +8WRpeyOh5xDZht/KGphZSQyjEpF/RVXWXstkqrKMZRZlKW0EcuBHX4EsTNuzRYC19ReD7d2/CM5M +u9u+iTR1Kws0r3YX4cMnlLXVzJPlAzUrbXmYAMYtpbbYT9QW +-----END CERTIFICATE----- diff --git a/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks b/src/test/resources/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks new file mode 100644 index 0000000000000000000000000000000000000000..8fabcdafd298e051b4432e8ede4721bdfafbbaad GIT binary patch literal 3202 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