SSL/TLS: Do not require keystore or truststore on for clients

This commit removes the requirement that a client using the SSLService must
have defined a keystore. Now for clients both the keystore and truststore are
optional; if neither are defined the system default trust managers will be used.

Closes elastic/elasticsearch#613

Original commit: elastic/x-pack-elasticsearch@1055a9666a
This commit is contained in:
jaymode 2015-01-22 10:27:50 -05:00
parent 2986502984
commit 97f229f667
5 changed files with 289 additions and 17 deletions

View File

@ -5,7 +5,6 @@
*/
package org.elasticsearch.shield.ssl;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.primitives.Ints;
@ -67,6 +66,78 @@ public class SSLService extends AbstractComponent {
return createSSLEngine(getSslContext(settings), ciphers, supportedProtocols, host, port);
}
//TODO this needs to be removed and the logic folded into the other methods of this class or we create a ClientSSLService class
public SSLEngine createClientSSLEngine() {
return createClientSSLEngine(null, -1);
}
//TODO this needs to be removed and the logic folded into the other methods of this class or we create a ClientSSLService class
public SSLEngine createClientSSLEngine(String host, int port) {
SSLContext sslContext = getClientSSLContext();
return createSSLEngine(sslContext, ciphers(), supportedProtocols(), host, port);
}
//TODO remove this when createClientSSLEngine is removed. Used for tests
SSLContext getClientSSLContext() {
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 keyPassword = componentSettings.get("keystore.key_password", keyStorePassword);
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
trustStorePath = keyStorePath;
trustStorePassword = keyStorePassword;
}
//protocols supported: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext
String sslProtocol = componentSettings.get("protocol", "TLS");
// no need for a complex key, same path + protocol define about reusability of a SSLContext
// also no need for pwd verification. If it worked before, it will work again
String key = keyStorePath + trustStorePath + sslProtocol;
SSLContext sslContext = sslContexts.get(key);
if (sslContext == null) {
logger.debug("using keystore[{}], key_algorithm[{}], truststore[{}], truststore_algorithm[{}], tls_protocol[{}]",
keyStorePath, keyStoreAlgorithm, trustStorePath, trustStoreAlgorithm, sslProtocol);
TrustManagerFactory trustFactory;
try {
trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
if (trustStorePath != null) {
trustFactory.init(readKeystore(trustStorePath, trustStorePassword));
} else {
trustFactory.init((KeyStore) null);
}
} catch (Exception e) {
throw new ElasticsearchSSLException("failed to initialize a TrustManagerFactory", e);
}
KeyManager[] keyManagers = null;
if (keyStorePath != null) {
if (keyStorePassword == null) {
throw new ShieldSettingsException("no keystore password configured");
}
keyManagers = createKeyManagerFactory(keyStorePath, keyStorePassword, keyStoreAlgorithm, keyPassword).getKeyManagers();
}
try {
sslContext = SSLContext.getInstance(sslProtocol);
sslContext.init(keyManagers, trustFactory.getTrustManagers(), null);
} catch (Exception e) {
throw new ElasticsearchSSLException("failed to initialize the SSLContext", e);
}
sslContexts.put(key, sslContext);
} else {
logger.trace("found keystore[{}], truststore[{}], tls_protocol[{}] in SSL context cache, reusing", keyStorePath, trustStorePath, sslProtocol);
}
return sslContext;
}
public SSLContext getSslContext() {
return getSslContext(ImmutableSettings.EMPTY);
}
@ -135,10 +206,9 @@ public class SSLService extends AbstractComponent {
}
private KeyManagerFactory createKeyManagerFactory(String keyStore, String keyStorePassword, String keyStoreAlgorithm, String keyPassword) {
try (FileInputStream in = new FileInputStream(keyStore)) {
try {
// Load KeyStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(in, keyStorePassword.toCharArray());
KeyStore ks = readKeystore(keyStore, keyStorePassword);
// Initialize KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm);
@ -163,17 +233,25 @@ public class SSLService extends AbstractComponent {
}
private TrustManagerFactory getTrustFactory(String trustStore, String trustStorePassword, String trustStoreAlgorithm) {
try (FileInputStream in = new FileInputStream(trustStore)) {
try {
// Load TrustStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray());
KeyStore ks = readKeystore(trustStore, trustStorePassword);
// 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);
throw new ElasticsearchSSLException("failed to initialize a TrustManagerFactory", e);
}
}
private KeyStore readKeystore(String path, String password) throws Exception {
try (FileInputStream in = new FileInputStream(path)) {
// Load TrustStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(in, password == null ? null : password.toCharArray());
return ks;
}
}
}

View File

@ -114,12 +114,12 @@ public class NettySecuredTransport extends NettyTransport {
SSLEngine sslEngine;
if (settings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) e.getValue();
sslEngine = sslService.createSSLEngine(ImmutableSettings.EMPTY, getHostname(inetSocketAddress), inetSocketAddress.getPort());
sslEngine = sslService.createClientSSLEngine(getHostname(inetSocketAddress), inetSocketAddress.getPort());
SSLParameters parameters = new SSLParameters();
parameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(parameters);
} else {
sslEngine = sslService.createSSLEngine();
sslEngine = sslService.createClientSSLEngine();
}
sslEngine.setUseClientMode(true);

View File

@ -5,14 +5,19 @@
*/
package org.elasticsearch.shield.ssl;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.Before;
import org.junit.Test;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSessionContext;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -126,4 +131,63 @@ public class SSLServiceTests extends ElasticsearchTestCase {
assertThat(context.getSessionCacheSize(), equalTo(300));
assertThat(context.getSessionTimeout(), equalTo(600));
}
@Test
public void testThatCreateClientSSLEngineWithoutAnySettingsWorks() throws Exception {
SSLService sslService = new SSLService(ImmutableSettings.EMPTY);
SSLEngine sslEngine = sslService.createClientSSLEngine();
assertThat(sslEngine, notNullValue());
}
@Test
public void testThatCreateClientSSLEngineWithOnlyTruststoreWorks() throws Exception {
SSLService sslService = new SSLService(settingsBuilder()
.put("shield.ssl.truststore.path", testnodeStore)
.put("shield.ssl.truststore.password", "testnode")
.build());
SSLEngine sslEngine = sslService.createClientSSLEngine();
assertThat(sslEngine, notNullValue());
}
@Test
public void testThatCreateClientSSLEngineWithOnlyKeystoreWorks() throws Exception {
SSLService sslService = new SSLService(settingsBuilder()
.put("shield.ssl.keystore.path", testnodeStore)
.put("shield.ssl.keystore.password", "testnode")
.build());
SSLEngine sslEngine = sslService.createClientSSLEngine();
assertThat(sslEngine, notNullValue());
}
@Test
@Network
public void testThatClientSSLContextWithoutSettingsWorks() throws Exception {
SSLService sslService = new SSLService(ImmutableSettings.EMPTY);
SSLContext sslContext = sslService.getClientSSLContext();
try (CloseableHttpClient client = HttpClients.custom().setSslcontext(sslContext).build()) {
// Execute a GET on a site known to have a valid certificate signed by a trusted public CA
// This will result in a SSLHandshakeException if the SSLContext does not trust the CA, but the default
// truststore trusts all common public CAs so the handshake will succeed
client.execute(new HttpGet("https://www.elasticsearch.com/"));
}
}
@Test
@Network
public void testThatClientSSLContextWithKeystoreDoesNotTrustAllPublicCAs() throws Exception {
SSLService sslService = new SSLService(settingsBuilder()
.put("shield.ssl.keystore.path", testnodeStore)
.put("shield.ssl.keystore.password", "testnode")
.build());
SSLContext sslContext = sslService.getSslContext();
try (CloseableHttpClient client = HttpClients.custom().setSslcontext(sslContext).build()) {
// Execute a GET on a site known to have a valid certificate signed by a trusted public CA
// This will result in a SSLHandshakeException because the truststore is the testnodestore, which doesn't
// trust any public CAs
client.execute(new HttpGet("https://www.elasticsearch.com/"));
fail("A SSLHandshakeException should have been thrown here");
} catch (Exception e) {
assertThat(e, instanceOf(SSLHandshakeException.class));
}
}
}

View File

@ -17,7 +17,9 @@ import org.elasticsearch.transport.Transport;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.elasticsearch.shield.transport.support.TransportProfileUtil.getProfilePort;
@ -32,22 +34,25 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
private static int randomClientPort;
private static int randomNonSslPort;
private static int randomNoClientAuthPort;
@BeforeClass
public static void getRandomPort() {
randomClientPort = randomIntBetween(49000, 65500); // ephemeral port
randomNonSslPort = randomIntBetween(49000, 65500);
randomNoClientAuthPort = randomIntBetween(49000, 65500);
}
@Override
protected Settings nodeSettings(int nodeOrdinal) {
String randomClientPortRange = randomClientPort + "-" + (randomClientPort+100);
String randomNonSslPortRange = randomNonSslPort + "-" + (randomNonSslPort+100);
String randomNoClientAuthPortRange = randomNoClientAuthPort + "-" + (randomNoClientAuthPort+100);
File store;
Path store;
try {
store = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks").toURI());
assertThat(store.exists(), is(true));
store = Paths.get(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks").toURI());
assertThat(Files.exists(store), is(true));
} catch (Exception e) {
throw new RuntimeException(e);
}
@ -57,11 +62,14 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
// 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.path", store.toAbsolutePath()) // settings for client truststore
.put("transport.profiles.client.shield.truststore.password", "testnode-client-profile")
.put("transport.profiles.no_ssl.port", randomNonSslPortRange)
.put("transport.profiles.no_ssl.bind_host", "localhost")
.put("transport.profiles.no_ssl.shield.ssl", "false")
.put("transport.profiles.no_client_auth.port", randomNoClientAuthPortRange)
.put("transport.profiles.no_client_auth.bind_host", "localhost")
.put("transport.profiles.no_client_auth.shield.ssl.client.auth", false)
.build();
}
@ -84,6 +92,14 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
assertGreenClusterState(internalCluster().transportClient());
}
@Test
public void testThatStandardTransportClientCanConnectToNoClientAuthProfile() throws Exception {
try(TransportClient transportClient = createTransportClient(ImmutableSettings.EMPTY)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_client_auth", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatStandardTransportClientCannotConnectToClientProfile() throws Exception {
try(TransportClient transportClient = createTransportClient(ImmutableSettings.EMPTY)) {
@ -92,6 +108,14 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatStandardTransportClientCannotConnectToNoSslProfile() throws Exception {
try (TransportClient transportClient = createTransportClient(ImmutableSettings.EMPTY)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_ssl", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test
public void testThatProfileTransportClientCanConnectToClientProfile() throws Exception {
Settings settings = ShieldSettingsSource.getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile");
@ -101,6 +125,15 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
}
}
@Test
public void testThatProfileTransportClientCanConnectToNoClientAuthProfile() throws Exception {
Settings settings = ShieldSettingsSource.getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile");
try (TransportClient transportClient = createTransportClient(settings)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_client_auth", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatProfileTransportClientCannotConnectToDefaultProfile() throws Exception {
Settings settings = ShieldSettingsSource.getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile");
@ -111,6 +144,15 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatProfileTransportClientCannotConnectToNoSslProfile() throws Exception {
Settings settings = ShieldSettingsSource.getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile");
try (TransportClient transportClient = createTransportClient(settings)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_ssl", internalCluster())));
transportClient.admin().cluster().prepareHealth().get();
}
}
@Test
public void testThatTransportClientCanConnectToNoSslProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
@ -124,8 +166,96 @@ public class SslMultiPortTests extends ShieldIntegrationTest {
}
@Test(expected = NoNodeAvailableException.class)
public void testThatStandardTransportClientCannotConnectToNoSslProfile() throws Exception {
try (TransportClient transportClient = createTransportClient(ImmutableSettings.EMPTY)) {
public void testThatTransportClientCannotConnectToDefaultProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("default", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatTransportClientCannotConnectToClientProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("client", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatTransportClientCannotConnectToNoClientAuthProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_client_auth", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test
public void testThatTransportClientWithOnlyTruststoreCanConnectToNoClientAuthProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.put("shield.transport.ssl", true)
.put("shield.ssl.truststore.path", Paths.get(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/truststore-testnode-only.jks").toURI()))
.put("shield.ssl.truststore.password", "truststore-testnode-only")
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_client_auth", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatTransportClientWithOnlyTruststoreCannotConnectToClientProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.put("shield.transport.ssl", true)
.put("shield.ssl.truststore.path", Paths.get(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/truststore-testnode-only.jks").toURI()))
.put("shield.ssl.truststore.password", "truststore-testnode-only")
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("client", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatTransportClientWithOnlyTruststoreCannotConnectToDefaultProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.put("shield.transport.ssl", true)
.put("shield.ssl.truststore.path", Paths.get(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/truststore-testnode-only.jks").toURI()))
.put("shield.ssl.truststore.password", "truststore-testnode-only")
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("default", internalCluster())));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatTransportClientWithOnlyTruststoreCannotConnectToNoSslProfile() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("shield.user", DEFAULT_USER_NAME + ":" + DEFAULT_PASSWORD)
.put("cluster.name", internalCluster().getClusterName())
.put("shield.transport.ssl", true)
.put("shield.ssl.truststore.path", Paths.get(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/truststore-testnode-only.jks").toURI()))
.put("shield.ssl.truststore.password", "truststore-testnode-only")
.build();
try (TransportClient transportClient = new TransportClient(settings, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", getProfilePort("no_ssl", internalCluster())));
assertGreenClusterState(transportClient);
}