Skip to content

Allow SSL configuration with the Elasticsearch CA fingerprint. #2540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/asciidoc/reference/elasticsearch-clients.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/main/asciidoc/reference/elasticsearch-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@
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;
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.
Expand Down Expand Up @@ -122,6 +120,12 @@ static ClientConfiguration create(InetSocketAddress socketAddress) {
*/
Optional<SSLContext> getSslContext();

/**
* @return the optional SHA-256 fingerprint of the self-signed http_ca.crt certificate output by Elasticsearch at
* startup time.
*/
Optional<String> getCaFingerprint();

/**
* Returns the {@link HostnameVerifier} to use. Can be {@link Optional#empty()} if not configured.
*
Expand Down Expand Up @@ -250,6 +254,15 @@ interface MaybeSecureClientConfigurationBuilder extends TerminalClientConfigurat
* @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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
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;
Expand All @@ -33,7 +32,6 @@
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}.
Expand All @@ -51,14 +49,15 @@ class ClientConfigurationBuilder
private final List<InetSocketAddress> 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<HttpHeaders> headersSupplier = HttpHeaders::new;
@Deprecated private final HttpClientConfigCallback httpClientConfigurer = httpClientBuilder -> httpClientBuilder;
private final List<ClientConfiguration.ClientConfigurationCallback<?>> clientConfigurers = new ArrayList<>();
Expand Down Expand Up @@ -138,10 +137,20 @@ public TerminalClientConfigurationBuilder usingSsl(SSLContext sslContext, Hostna
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) {

Expand Down Expand Up @@ -228,8 +237,12 @@ public ClientConfiguration build() {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,28 @@ class DefaultClientConfiguration implements ClientConfiguration {
private final List<InetSocketAddress> 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<HttpHeaders> headersSupplier;
private final List<ClientConfigurationCallback<?>> clientConfigurers;

DefaultClientConfiguration(List<InetSocketAddress> 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<ClientConfigurationCallback<?>> clientConfigurers,
Supplier<HttpHeaders> headersSupplier) {

this.hosts = List.copyOf(hosts);
this.headers = headers;
this.useSsl = useSsl;
this.sslContext = sslContext;
this.caFingerprint = caFingerprint;
this.soTimeout = soTimeout;
this.connectTimeout = connectTimeout;
this.pathPrefix = pathPrefix;
Expand Down Expand Up @@ -92,6 +94,11 @@ public Optional<SSLContext> getSslContext() {
return Optional.ofNullable(this.sslContext);
}

@Override
public Optional<String> getCaFingerprint() {
return Optional.ofNullable(this.caFingerprint);
}

@Override
public Optional<HostnameVerifier> getHostNameVerifier() {
return Optional.ofNullable(this.hostnameVerifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -34,13 +35,7 @@
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;
Expand Down Expand Up @@ -197,6 +192,10 @@ private static RestClientBuilder getRestClientBuilder(ClientConfiguration client
}

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()));
Expand Down