diff --git a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientConfigurations.java b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientConfigurations.java index 1866476..010a7e4 100644 --- a/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientConfigurations.java +++ b/spring-data-opensearch-starter/src/main/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientConfigurations.java @@ -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; @@ -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; @@ -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; @@ -48,8 +54,8 @@ static class RestClientBuilderConfiguration { } @Bean - RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(OpenSearchProperties properties) { - return new DefaultRestClientBuilderCustomizer(properties, this.connectionDetails); + RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(OpenSearchProperties properties, ObjectProvider sslBundles) { + return new DefaultRestClientBuilderCustomizer(properties, this.connectionDetails, sslBundles); } @Bean @@ -136,9 +142,12 @@ static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCust private final OpenSearchConnectionDetails connectionDetails; - DefaultRestClientBuilderCustomizer(OpenSearchProperties properties, OpenSearchConnectionDetails connectionDetails) { + private final ObjectProvider sslBundles; + + DefaultRestClientBuilderCustomizer(OpenSearchProperties properties, OpenSearchConnectionDetails connectionDetails, ObjectProvider sslBundles) { this.properties = properties; this.connectionDetails = connectionDetails; + this.sslBundles = sslBundles; } @Override @@ -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 @@ -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 { diff --git a/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationIntegrationTests.java b/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationIntegrationTests.java index 139d83b..f4e5ce0 100644 --- a/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationIntegrationTests.java @@ -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; @@ -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)); @@ -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); + }); + } } diff --git a/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationTests.java b/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationTests.java index 6f8294a..5ef2dc7 100644 --- a/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationTests.java +++ b/spring-data-opensearch-starter/src/test/java/org/opensearch/spring/boot/autoconfigure/OpenSearchRestClientAutoConfigurationTests.java @@ -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; @@ -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}. @@ -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 registry = (Registry) 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 { diff --git a/spring-data-opensearch-starter/src/test/resources/opensearch-demo-ca.pem b/spring-data-opensearch-starter/src/test/resources/opensearch-demo-ca.pem new file mode 100644 index 0000000..009ebdc --- /dev/null +++ b/spring-data-opensearch-starter/src/test/resources/opensearch-demo-ca.pem @@ -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-----