From ef979e49399b7d179f8b00d017b2bcab78b9ef71 Mon Sep 17 00:00:00 2001 From: jaymode Date: Tue, 20 Jan 2015 13:40:58 -0500 Subject: [PATCH] SSL/TLS: Only use TLS protocols by default Only enables TLSv1, TLSv1.1, and TLSv1.2 protocols for transport, http, and ldaps. The supported protocols are configurable in case one of these protocols is found to be insecure in the future. Closes elastic/elasticsearch#594 Original commit: elastic/x-pack-elasticsearch@d4556091effd72f19c87f79184f043add230f1ff --- .../ldap/AbstractLdapSslSocketFactory.java | 2 + ...HostnameVerifyingLdapSslSocketFactory.java | 1 + .../elasticsearch/shield/ssl/SSLService.java | 22 ++++- .../shield/ssl/SSLServiceTests.java | 11 +++ .../transport/ssl/SslIntegrationTests.java | 80 ++++++++++++++----- 5 files changed, 92 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapSslSocketFactory.java b/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapSslSocketFactory.java index ab56cd8d55c..b58a839ba53 100644 --- a/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapSslSocketFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapSslSocketFactory.java @@ -78,5 +78,7 @@ public abstract class AbstractLdapSslSocketFactory extends SocketFactory { * @param sslSocket */ protected void configureSSLSocket(SSLSocket sslSocket) { + sslSocket.setEnabledProtocols(sslService.supportedProtocols()); + sslSocket.setEnabledCipherSuites(sslService.ciphers()); } } diff --git a/src/main/java/org/elasticsearch/shield/authc/support/ldap/HostnameVerifyingLdapSslSocketFactory.java b/src/main/java/org/elasticsearch/shield/authc/support/ldap/HostnameVerifyingLdapSslSocketFactory.java index 8c4dd37a4a3..f91b972ad3f 100644 --- a/src/main/java/org/elasticsearch/shield/authc/support/ldap/HostnameVerifyingLdapSslSocketFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/support/ldap/HostnameVerifyingLdapSslSocketFactory.java @@ -63,6 +63,7 @@ public class HostnameVerifyingLdapSslSocketFactory extends AbstractLdapSslSocket */ @Override protected void configureSSLSocket(SSLSocket sslSocket) { + super.configureSSLSocket(sslSocket); sslSocket.setSSLParameters(sslParameters); } } diff --git a/src/main/java/org/elasticsearch/shield/ssl/SSLService.java b/src/main/java/org/elasticsearch/shield/ssl/SSLService.java index 221c6256040..8e957f2e4ab 100644 --- a/src/main/java/org/elasticsearch/shield/ssl/SSLService.java +++ b/src/main/java/org/elasticsearch/shield/ssl/SSLService.java @@ -26,6 +26,7 @@ import java.util.Map; 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_SUPPORTED_PROTOCOLS = new String[] {"TLSv1", "TLSv1.1", "TLSv1.2"}; private Map sslContexts = ConcurrentCollections.newConcurrentMap(); @@ -41,6 +42,14 @@ public class SSLService extends AbstractComponent { return getSslContext(ImmutableSettings.EMPTY).getSocketFactory(); } + public String[] supportedProtocols() { + return componentSettings.getAsArray("supported_protocols", DEFAULT_SUPPORTED_PROTOCOLS); + } + + public String[] ciphers() { + return componentSettings.getAsArray("ciphers", DEFAULT_CIPHERS); + } + public SSLEngine createSSLEngine() { return createSSLEngine(ImmutableSettings.EMPTY); } @@ -50,8 +59,9 @@ public class SSLService extends AbstractComponent { } public SSLEngine createSSLEngine(Settings settings, String host, int port) { - String[] ciphers = settings.getAsArray("ciphers", componentSettings.getAsArray("ciphers", DEFAULT_CIPHERS)); - return createSSLEngine(getSslContext(settings), ciphers, host, port); + String[] ciphers = settings.getAsArray("ciphers", ciphers()); + String[] supportedProtocols = settings.getAsArray("supported_protocols", supportedProtocols()); + return createSSLEngine(getSslContext(settings), ciphers, supportedProtocols, host, port); } public SSLContext getSslContext() { @@ -103,13 +113,19 @@ public class SSLService extends AbstractComponent { return sslContext; } - private SSLEngine createSSLEngine(SSLContext sslContext, String[] ciphers, String host, int port) { + private SSLEngine createSSLEngine(SSLContext sslContext, String[] ciphers, String[] supportedProtocols, String host, int port) { SSLEngine sslEngine = sslContext.createSSLEngine(host, port); try { sslEngine.setEnabledCipherSuites(ciphers); } catch (Throwable t) { throw new ElasticsearchSSLException("failed loading cipher suites [" + Arrays.asList(ciphers) + "]", t); } + + try { + sslEngine.setEnabledProtocols(supportedProtocols); + } catch (IllegalArgumentException e) { + throw new ElasticsearchSSLException("failed setting supported protocols [" + Arrays.asList(supportedProtocols) + "]", e); + } return sslEngine; } diff --git a/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java b/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java index 0de507ae371..c8a2ed33ee8 100644 --- a/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java +++ b/src/test/java/org/elasticsearch/shield/ssl/SSLServiceTests.java @@ -14,6 +14,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; import static org.hamcrest.Matchers.*; @@ -89,4 +90,14 @@ public class SSLServiceTests extends ElasticsearchTestCase { .put("shield.ssl.keystore.password", "testnode") .build()).createSSLEngine(); } + + @Test + public void testThatSSLv3IsNotEnabled() throws Exception { + SSLService sslService = new SSLService(settingsBuilder() + .put("shield.ssl.keystore.path", testnodeStore) + .put("shield.ssl.keystore.password", "testnode") + .build()); + SSLEngine engine = sslService.createSSLEngine(); + assertThat(Arrays.asList(engine.getEnabledProtocols()), not(hasItem("SSLv3"))); + } } 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 799f8be02de..91cedf02f9c 100644 --- a/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/ssl/SslIntegrationTests.java @@ -6,6 +6,10 @@ package org.elasticsearch.shield.transport.ssl; import com.google.common.base.Charsets; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.elasticsearch.client.transport.NoNodeAvailableException; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.io.Streams; @@ -25,6 +29,8 @@ import javax.net.ssl.*; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.util.Locale; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; @@ -35,6 +41,20 @@ import static org.hamcrest.Matchers.*; @ClusterScope(scope = Scope.SUITE) public class SslIntegrationTests extends ShieldIntegrationTest { + private TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } + }; + @Override protected Settings nodeSettings(int nodeOrdinal) { return ImmutableSettings.builder().put(super.nodeSettings(nodeOrdinal)) @@ -64,27 +84,28 @@ public class SslIntegrationTests extends ShieldIntegrationTest { } } + // no SSL exception as this is the exception is returned when connecting + @Test(expected = NoNodeAvailableException.class) + public void testThatTransportClientUsingSSLv3ProtocolIsRejected() { + try(TransportClient transportClient = new TransportClient(settingsBuilder() + .put(transportClientSettings()) + .put("name", "programmatic_transport_client") + .put("cluster.name", internalCluster().getClusterName()) + .putArray("shield.ssl.supported_protocols", new String[]{"SSLv3"}) + .build())) { + + TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress(); + transportClient.addTransportAddress(transportAddress); + + transportClient.admin().cluster().prepareHealth().get(); + } + } + @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()); + sc.init(null, trustAllCerts, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); // totally secure HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { @@ -94,10 +115,7 @@ public class SslIntegrationTests extends ShieldIntegrationTest { } }); - 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()); + String url = getNodeUrl(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestProperty(UsernamePasswordToken.BASIC_AUTH_HEADER, UsernamePasswordToken.basicAuthHeaderValue(nodeClientUsername(), nodeClientPassword())); @@ -107,4 +125,24 @@ public class SslIntegrationTests extends ShieldIntegrationTest { String data = Streams.copyToString(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8)); assertThat(data, containsString("You Know, for Search")); } + + @Test + public void testThatHttpUsingSSLv3IsRejected() throws Exception { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(sslContext, new String[]{"SSLv3"}, null, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + try (CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(sf).build()) { + client.execute(new HttpGet(getNodeUrl())); + fail("Expected a connection error due to SSLv3 not being supported by default"); + } catch (Exception e) { + assertThat(e, is(instanceOf(SSLHandshakeException.class))); + } + } + + private String getNodeUrl() { + TransportAddress transportAddress = internalCluster().getInstance(HttpServerTransport.class).boundAddress().boundAddress(); + assertThat(transportAddress, is(instanceOf(InetSocketTransportAddress.class))); + InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) transportAddress; + return String.format(Locale.ROOT, "https://%s:%s/", InetAddresses.toUriString(inetSocketTransportAddress.address().getAddress()), inetSocketTransportAddress.address().getPort()); + } }