Skip to content

Commit

Permalink
add ssl bundle support for autoconfigured opensearch client (#397)
Browse files Browse the repository at this point in the history
* add ssl bundle support for autoconfigured opensearch client

Signed-off-by: Aivis Henins <[email protected]>

* fix tests

- fix formatting
- add certificate used by OpenSearch 1.3.x
- don't need to set ssl context when ssl strategy is set

Signed-off-by: Aivis Henins <[email protected]>

---------

Signed-off-by: Aivis Henins <[email protected]>
  • Loading branch information
aivish authored Dec 27, 2024
1 parent d18fb7d commit ebef1d5
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
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;
Expand All @@ -16,6 +18,7 @@
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.opensearch.client.RestClient;
import org.opensearch.client.RestClientBuilder;
import org.opensearch.client.sniff.Sniffer;
Expand All @@ -25,6 +28,9 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
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;
Expand All @@ -48,8 +54,8 @@ static class RestClientBuilderConfiguration {
}

@Bean
RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(OpenSearchProperties properties) {
return new DefaultRestClientBuilderCustomizer(properties, this.connectionDetails);
RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(OpenSearchProperties properties, ObjectProvider<SslBundles> sslBundles) {
return new DefaultRestClientBuilderCustomizer(properties, this.connectionDetails, sslBundles);
}

@Bean
Expand Down Expand Up @@ -136,9 +142,12 @@ static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCust

private final OpenSearchConnectionDetails connectionDetails;

DefaultRestClientBuilderCustomizer(OpenSearchProperties properties, OpenSearchConnectionDetails connectionDetails) {
private final ObjectProvider<SslBundles> sslBundles;

DefaultRestClientBuilderCustomizer(OpenSearchProperties properties, OpenSearchConnectionDetails connectionDetails, ObjectProvider<SslBundles> sslBundles) {
this.properties = properties;
this.connectionDetails = connectionDetails;
this.sslBundles = sslBundles;
}

@Override
Expand All @@ -150,6 +159,11 @@ public void customize(HttpAsyncClientBuilder builder) {
map.from(this.properties::isSocketKeepAlive)
.to((keepAlive) -> builder.setDefaultIOReactorConfig(
IOReactorConfig.custom().setSoKeepAlive(keepAlive).build()));

String sslBundleName = properties.getRestclient().getSsl().getBundle();
if (StringUtils.hasText(sslBundleName)) {
this.configureSsl(builder, sslBundles.getObject().getBundle(sslBundleName));
}
}

@Override
Expand All @@ -163,6 +177,13 @@ public void customize(RequestConfig.Builder builder) {
.asInt(Duration::toMillis)
.to(builder::setSocketTimeout);
}

private void configureSsl(HttpAsyncClientBuilder builder, SslBundle sslBundle) {
SSLContext sslcontext = sslBundle.createSslContext();
SslOptions sslOptions = sslBundle.getOptions();

builder.setSSLStrategy(new SSLIOSessionStrategy(sslcontext, sslOptions.getEnabledProtocols(), sslOptions.getCiphers(), (HostnameVerifier) null));
}
}

private static class ConnectionsDetailsCredentialsProvider extends BasicCredentialsProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.opensearch.client.RestClient;
import org.opensearch.testcontainers.OpensearchContainer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
Expand All @@ -34,6 +35,12 @@ class OpenSearchRestClientAutoConfigurationIntegrationTests extends AbstractOpen
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

@Container
static final OpensearchContainer<?> secureOpensearch = new OpensearchContainer<>(getDockerImageName())
.withSecurityEnabled()
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10));

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OpenSearchRestClientAutoConfiguration.class));

Expand Down Expand Up @@ -76,4 +83,27 @@ void restClientCanQueryOpensearchNode() {
}
});
}

@Test
void restClientWithSslCanConnectToOpensearch() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class))
.withPropertyValues(
"opensearch.uris=" + secureOpensearch.getHttpHostAddress(),
"opensearch.connection-timeout=120s",
"opensearch.socket-timeout=120s",
"opensearch.username=" + secureOpensearch.getUsername(),
"opensearch.password=" + secureOpensearch.getPassword(),
"opensearch.restclient.ssl.bundle=opensearch-demo-ca",
"spring.ssl.bundle.pem.opensearch-demo-ca.truststore.certificate=classpath:opensearch-demo-ca.pem"
)
.run((context) -> {
final RestClient client = context.getBean(RestClient.class);
final Request request = new Request("GET", "/");

final Response response = client.performRequest(request);

assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
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.junit.jupiter.api.Test;
import org.opensearch.client.Node;
Expand All @@ -25,10 +27,12 @@
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
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;

/**
* Tests for {@link OpenSearchRestClientAutoConfiguration}.
Expand Down Expand Up @@ -259,6 +263,34 @@ void configureWhenCustomSnifferShouldBackOff() {
});
}

@Test
void configureWithSslBundle() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class))
.withPropertyValues(
"opensearch.restclient.ssl.bundle=opensearch-ca",
"spring.ssl.bundle.pem.opensearch-ca.truststore.certificate=classpath:opensearch-demo-ca.pem",
"spring.ssl.bundle.pem.opensearch-ca.options.ciphers=DESede",
"spring.ssl.bundle.pem.opensearch-ca.options.enabled-protocols=TLSv1.3"
)
.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 BuilderCustomizerConfiguration {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL
BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt
cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl
IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v
dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm
iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ
RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290
IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU
j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4
U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg
vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA
WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969
VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW
MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU
F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4
uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ
k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD
VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg
Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN
AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC
YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V
6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG
1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq
qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov
rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm
iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ
RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290
IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy
MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL
BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV
BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt
9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8
Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL
gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl
ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq
eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw
gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB
GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs
ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw
HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv
78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg
MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq
AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI
hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0
5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy
8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr
XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA
1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t
e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs=
-----END CERTIFICATE-----

0 comments on commit ebef1d5

Please sign in to comment.