Add SSL bundle support to Elasticsearch auto-configuration

Update Elasticsearch RestClient auto-configuration so that an SSL
can be configured via an SSL bundle.

Closes gh-35155
This commit is contained in:
Phillip Webb 2023-04-25 17:18:00 -07:00
parent e9a85b91a8
commit 423c60acfa
4 changed files with 86 additions and 6 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -133,10 +133,16 @@ public class ElasticsearchProperties {
private final Sniffer sniffer = new Sniffer();
private final Ssl ssl = new Ssl();
public Sniffer getSniffer() {
return this.sniffer;
}
public Ssl getSsl() {
return this.ssl;
}
public static class Sniffer {
/**
@ -167,6 +173,23 @@ public class ElasticsearchProperties {
}
public static class Ssl {
/**
* SSL bundle name.
*/
private String bundle;
public String getBundle() {
return this.bundle;
}
public void setBundle(String bundle) {
this.bundle = bundle;
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientBuilderConfiguration;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientConfiguration;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientSnifferConfiguration;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
@ -34,7 +35,7 @@ import org.springframework.context.annotation.Import;
* @author Stephane Nicoll
* @since 2.1.0
*/
@AutoConfiguration
@AutoConfiguration(after = SslAutoConfiguration.class)
@ConditionalOnClass(RestClientBuilder.class)
@EnableConfigurationProperties(ElasticsearchProperties.class)
@Import({ RestClientBuilderConfiguration.class, RestClientConfiguration.class, RestClientSnifferConfiguration.class })

View File

@ -19,8 +19,12 @@ package org.springframework.boot.autoconfigure.elasticsearch;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
@ -29,6 +33,7 @@ import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.sniff.Sniffer;
@ -41,6 +46,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandi
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
@ -81,13 +89,17 @@ class ElasticsearchRestClientConfigurations {
@Bean
RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchConnectionDetails connectionDetails,
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) {
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers, ObjectProvider<SslBundles> sslBundles) {
RestClientBuilder builder = RestClient.builder(connectionDetails.getNodes()
.stream()
.map((node) -> new HttpHost(node.hostname(), node.port(), node.protocol().getScheme()))
.toArray(HttpHost[]::new));
builder.setHttpClientConfigCallback((httpClientBuilder) -> {
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder));
String sslBundleName = this.properties.getRestclient().getSsl().getBundle();
if (StringUtils.hasText(sslBundleName)) {
configureSsl(httpClientBuilder, sslBundles.getObject().getBundle(sslBundleName));
}
return httpClientBuilder;
});
builder.setRequestConfigCallback((requestConfigBuilder) -> {
@ -102,6 +114,19 @@ class ElasticsearchRestClientConfigurations {
return builder;
}
private void configureSsl(HttpAsyncClientBuilder httpClientBuilder, SslBundle sslBundle) {
SSLContext sslcontext = sslBundle.createSslContext();
SslOptions sslOptions = sslBundle.getOptions();
String[] enabledProtocols = toArray(sslOptions.getEnabledProtocols());
String[] ciphers = toArray(sslOptions.getCiphers());
httpClientBuilder.setSSLStrategy(
new SSLIOSessionStrategy(sslcontext, enabledProtocols, ciphers, (HostnameVerifier) null));
}
private static String[] toArray(Set<String> set) {
return (set != null) ? set.toArray(String[]::new) : null;
}
}
@Configuration(proxyBeanMethods = false)

View File

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.elasticsearch;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpHost;
@ -24,7 +25,9 @@ import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.elasticsearch.client.Node;
import org.elasticsearch.client.RestClient;
@ -35,10 +38,12 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.PropertiesElasticsearchConnectionDetails;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
@ -57,8 +62,8 @@ import static org.mockito.Mockito.mock;
*/
class ElasticsearchRestClientAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void configureShouldCreateRestClientBuilderAndRestClient() {
@ -282,6 +287,32 @@ class ElasticsearchRestClientAutoConfigurationTests {
});
}
@Test
@SuppressWarnings("unchecked")
void configureWithSslBundle() {
List<String> properties = new ArrayList<>();
properties.add("spring.elasticsearch.restclient.ssl.bundle=mybundle");
properties.add("spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks");
properties.add("spring.ssl.bundle.jks.mybundle.options.ciphers=DESede");
properties.add("spring.ssl.bundle.jks.mybundle.options.enabled-protocols=TLSv1.3");
this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> {
assertThat(context).hasSingleBean(RestClient.class);
RestClient restClient = context.getBean(RestClient.class);
Object client = ReflectionTestUtils.getField(restClient, "client");
Object connmgr = ReflectionTestUtils.getField(client, "connmgr");
Registry<SchemeIOSessionStrategy> registry = (Registry<SchemeIOSessionStrategy>) ReflectionTestUtils
.getField(connmgr, "ioSessionFactoryRegistry");
SchemeIOSessionStrategy strategy = registry.lookup("https");
assertThat(strategy).extracting("sslContext").isNotNull();
assertThat(strategy).extracting("supportedCipherSuites")
.asInstanceOf(InstanceOfAssertFactories.ARRAY)
.containsExactly("DESede");
assertThat(strategy).extracting("supportedProtocols")
.asInstanceOf(InstanceOfAssertFactories.ARRAY)
.containsExactly("TLSv1.3");
});
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsConfiguration {