From 406961c13ba0be437a447e70b6a55b594e7751dc Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Sat, 29 Apr 2023 18:43:50 +0200 Subject: [PATCH] Allow SSL configuration with the Elasticsearch CA fingerprint. Original Pull Request #2540 Closes #2539 --- .../reference/elasticsearch-clients.adoc | 2 +- .../asciidoc/reference/elasticsearch-new.adoc | 1 + .../client/ClientConfiguration.java | 17 +++++++- .../client/ClientConfigurationBuilder.java | 39 ++++++++++++------- .../client/DefaultClientConfiguration.java | 19 ++++++--- .../client/elc/ElasticsearchClients.java | 13 +++---- 6 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/main/asciidoc/reference/elasticsearch-clients.adoc b/src/main/asciidoc/reference/elasticsearch-clients.adoc index 04fe74b16..4760d09d4 100644 --- a/src/main/asciidoc/reference/elasticsearch-clients.adoc +++ b/src/main/asciidoc/reference/elasticsearch-clients.adoc @@ -240,7 +240,7 @@ ClientConfiguration clientConfiguration = ClientConfiguration.builder() <.> Define default headers, if they need to be customized <.> Use the builder to provide cluster addresses, set default `HttpHeaders` or enable SSL. -<.> Optionally enable SSL. +<.> Optionally enable SSL. There exist overloads of this function that can take a `SSLContext` or as an alternative the fingerprint of the certificate as it is output by Elasticsearch 8 on startup. <.> Optionally set a proxy. <.> Optionally set a path prefix, mostly used when different clusters a behind some reverse proxy. <.> Set the connection timeout. diff --git a/src/main/asciidoc/reference/elasticsearch-new.adoc b/src/main/asciidoc/reference/elasticsearch-new.adoc index 7d4decbc8..a8a7bd25f 100644 --- a/src/main/asciidoc/reference/elasticsearch-new.adoc +++ b/src/main/asciidoc/reference/elasticsearch-new.adoc @@ -5,6 +5,7 @@ == New in Spring Data Elasticsearch 5.1 * Upgrade to Elasticsearch 8.7.0 +* Allow specification of the TLS certificate when connecting to an Elasticsearch 8 cluster [[new-features.5-0-0]] == New in Spring Data Elasticsearch 5.0 diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java index a210cd851..ff802c3ab 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java @@ -20,7 +20,6 @@ import java.net.SocketAddress; import java.time.Duration; import java.util.List; import java.util.Optional; -import java.util.function.Function; import java.util.function.Supplier; import javax.net.ssl.HostnameVerifier; @@ -28,7 +27,6 @@ import javax.net.ssl.SSLContext; import org.springframework.data.elasticsearch.support.HttpHeaders; import org.springframework.lang.Nullable; -import org.springframework.web.reactive.function.client.WebClient; /** * Configuration interface exposing common client configuration properties for Elasticsearch clients. @@ -122,6 +120,12 @@ public interface ClientConfiguration { */ Optional getSslContext(); + /** + * @return the optional SHA-256 fingerprint of the self-signed http_ca.crt certificate output by Elasticsearch at + * startup time. + */ + Optional getCaFingerprint(); + /** * Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured. * @@ -250,6 +254,15 @@ public interface ClientConfiguration { * @return the {@link TerminalClientConfigurationBuilder}. */ TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, HostnameVerifier hostnameVerifier); + + /** + * Connect via https using a SSLContext that is build from the given certificate fingerprint. + * + * @param caFingerprint the SHA-256 fingerprint of the self-signed http_ca.crt certificate output by Elasticsearch + * at startup time. + * @return the {@link TerminalClientConfigurationBuilder}. + */ + TerminalClientConfigurationBuilder usingSsl(String caFingerprint); } /** diff --git a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java index 886491bbd..beb3097bd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java @@ -20,7 +20,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Function; import java.util.function.Supplier; import javax.net.ssl.HostnameVerifier; @@ -33,7 +32,6 @@ import org.springframework.data.elasticsearch.client.ClientConfiguration.Termina import org.springframework.data.elasticsearch.support.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.web.reactive.function.client.WebClient; /** * Default builder implementation for {@link ClientConfiguration}. @@ -51,14 +49,15 @@ class ClientConfigurationBuilder private final List hosts = new ArrayList<>(); private HttpHeaders headers = new HttpHeaders(); private boolean useSsl; - private @Nullable SSLContext sslContext; - private @Nullable HostnameVerifier hostnameVerifier; + @Nullable private SSLContext sslContext; + @Nullable private String caFingerprint; + @Nullable private HostnameVerifier hostnameVerifier; private Duration connectTimeout = Duration.ofSeconds(10); private Duration soTimeout = Duration.ofSeconds(5); - private @Nullable String username; - private @Nullable String password; - private @Nullable String pathPrefix; - private @Nullable String proxy; + @Nullable private String username; + @Nullable private String password; + @Nullable private String pathPrefix; + @Nullable private String proxy; private Supplier headersSupplier = HttpHeaders::new; @Deprecated private final HttpClientConfigCallback httpClientConfigurer = httpClientBuilder -> httpClientBuilder; private final List> clientConfigurers = new ArrayList<>(); @@ -138,10 +137,20 @@ class ClientConfigurationBuilder return this; } + @Override + public TerminalClientConfigurationBuilder usingSsl(String caFingerprint) { + + Assert.notNull(caFingerprint, "caFingerprint must not be null"); + + this.useSsl = true; + this.caFingerprint = caFingerprint; + return this; + } + /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withDefaultHeaders(org.springframework.http.HttpHeaders) - */ + * (non-Javadoc) + * @see org.springframework.data.elasticsearch.client.ClientConfiguration.TerminalClientConfigurationBuilder#withDefaultHeaders(org.springframework.http.HttpHeaders) + */ @Override public TerminalClientConfigurationBuilder withDefaultHeaders(HttpHeaders defaultHeaders) { @@ -228,8 +237,12 @@ class ClientConfigurationBuilder headers.setBasicAuth(username, password); } - return new DefaultClientConfiguration(hosts, headers, useSsl, sslContext, soTimeout, connectTimeout, pathPrefix, - hostnameVerifier, proxy, httpClientConfigurer, clientConfigurers, headersSupplier); + if (sslContext != null && caFingerprint != null) { + throw new IllegalArgumentException("Either SSLContext or caFingerprint must be set, but not both"); + } + + return new DefaultClientConfiguration(hosts, headers, useSsl, sslContext, caFingerprint, soTimeout, connectTimeout, + pathPrefix, hostnameVerifier, proxy, httpClientConfigurer, clientConfigurers, headersSupplier); } private static InetSocketAddress parse(String hostAndPort) { diff --git a/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java index ad768187a..599af28e3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java @@ -42,19 +42,20 @@ class DefaultClientConfiguration implements ClientConfiguration { private final List hosts; private final HttpHeaders headers; private final boolean useSsl; - private final @Nullable SSLContext sslContext; + @Nullable private final SSLContext sslContext; + @Nullable private final String caFingerprint; private final Duration soTimeout; private final Duration connectTimeout; - private final @Nullable String pathPrefix; - private final @Nullable HostnameVerifier hostnameVerifier; - private final @Nullable String proxy; + @Nullable private final String pathPrefix; + @Nullable private final HostnameVerifier hostnameVerifier; + @Nullable private final String proxy; private final HttpClientConfigCallback httpClientConfigurer; private final Supplier headersSupplier; private final List> clientConfigurers; DefaultClientConfiguration(List hosts, HttpHeaders headers, boolean useSsl, - @Nullable SSLContext sslContext, Duration soTimeout, Duration connectTimeout, @Nullable String pathPrefix, - @Nullable HostnameVerifier hostnameVerifier, @Nullable String proxy, + @Nullable SSLContext sslContext, @Nullable String caFingerprint, Duration soTimeout, Duration connectTimeout, + @Nullable String pathPrefix, @Nullable HostnameVerifier hostnameVerifier, @Nullable String proxy, HttpClientConfigCallback httpClientConfigurer, List> clientConfigurers, Supplier headersSupplier) { @@ -62,6 +63,7 @@ class DefaultClientConfiguration implements ClientConfiguration { this.headers = headers; this.useSsl = useSsl; this.sslContext = sslContext; + this.caFingerprint = caFingerprint; this.soTimeout = soTimeout; this.connectTimeout = connectTimeout; this.pathPrefix = pathPrefix; @@ -92,6 +94,11 @@ class DefaultClientConfiguration implements ClientConfiguration { return Optional.ofNullable(this.sslContext); } + @Override + public Optional getCaFingerprint() { + return Optional.ofNullable(this.caFingerprint); + } + @Override public Optional getHostNameVerifier() { return Optional.ofNullable(this.hostnameVerifier); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java index 58a2c4273..2549d7a17 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchClients.java @@ -19,6 +19,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.TransportUtils; import co.elastic.clients.transport.Version; import co.elastic.clients.transport.rest_client.RestClientOptions; import co.elastic.clients.transport.rest_client.RestClientTransport; @@ -34,13 +35,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.HttpResponse; -import org.apache.http.HttpResponseInterceptor; +import org.apache.http.*; import org.apache.http.client.config.RequestConfig; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; @@ -197,6 +192,10 @@ public final class ElasticsearchClients { } builder.setHttpClientConfigCallback(clientBuilder -> { + if (clientConfiguration.getCaFingerprint().isPresent()) { + clientBuilder + .setSSLContext(TransportUtils.sslContextFromCaFingerprint(clientConfiguration.getCaFingerprint().get())); + } clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext); clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier); clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier()));