From 17eea007006322284231fbc498f7fe02c8475fe0 Mon Sep 17 00:00:00 2001 From: Finn Carroll Date: Wed, 9 Apr 2025 15:24:16 -0700 Subject: [PATCH] Enable TLS for SecureNetty4GrpcServerTransport - Adds SecureAuxTransportSettingsProvider to provide aux transports access to a javax SSLContext and cipher/client auth params for configuring TLS. - Implements SecureNetty4GrpcServerTransport to consume a SecureAuxTransportSettingsProvider for a TLS enabled gRPC transport. - Add aux transport type settings and port setttings for new secure transport. - Add logic to detect and register secure aux transports provided by plugins. - Integration tests for SecureNetty4GrpcServerTransport basic client cert authentication. Signed-off-by: Finn Carroll --- CHANGELOG.md | 1 + .../arrow/flight/bootstrap/FlightService.java | 1 + plugins/transport-grpc/README.md | 42 ++++ plugins/transport-grpc/build.gradle | 11 +- ....java => Netty4GrpcServerTransportIT.java} | 45 +++- .../SecureNetty4GrpcServerTransportIT.java | 223 ++++++++++++++++++ .../plugin/transport/grpc/GrpcPlugin.java | 41 +++- .../grpc/Netty4GrpcServerTransport.java | 47 +++- .../ssl/SecureNetty4GrpcServerTransport.java | 130 ++++++++++ .../transport/grpc/ssl/package-info.java | 12 + .../transport/grpc/GrpcPluginTests.java | 28 ++- .../grpc/Netty4GrpcServerTransportTests.java | 56 +++-- .../transport/grpc/ssl/NettyGrpcClient.java | 168 +++++++++++++ .../SecureNetty4GrpcServerTransportTests.java | 157 ++++++++++++ .../grpc/ssl/SecureSettingsHelpers.java | 169 +++++++++++++ .../src/test/resources/README.txt | 26 ++ .../test/resources/netty4-client-secure.jks | Bin 0 -> 2772 bytes .../test/resources/netty4-server-secure.jks | Bin 0 -> 2772 bytes .../common/network/NetworkModule.java | 31 +++ .../org/opensearch/plugins/NetworkPlugin.java | 22 ++ .../SecureAuxTransportSettingsProvider.java | 52 ++++ .../plugins/SecureSettingsFactory.java | 7 + .../transport/TransportAdapterProvider.java | 2 +- .../common/network/NetworkModuleTests.java | 7 + 24 files changed, 1239 insertions(+), 39 deletions(-) create mode 100644 plugins/transport-grpc/README.md rename plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/{GrpcTransportIT.java => Netty4GrpcServerTransportIT.java} (52%) create mode 100644 plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportIT.java create mode 100644 plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/SecureNetty4GrpcServerTransport.java create mode 100644 plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/package-info.java create mode 100644 plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/NettyGrpcClient.java create mode 100644 plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportTests.java create mode 100644 plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureSettingsHelpers.java create mode 100644 plugins/transport-grpc/src/test/resources/README.txt create mode 100644 plugins/transport-grpc/src/test/resources/netty4-client-secure.jks create mode 100644 plugins/transport-grpc/src/test/resources/netty4-server-secure.jks create mode 100644 server/src/main/java/org/opensearch/plugins/SecureAuxTransportSettingsProvider.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c764c495051b6..39874bf57d94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [Security Manager Replacement] Enhance Java Agent to intercept Runtime::halt ([#17757](https://github.com/opensearch-project/OpenSearch/pull/17757)) - [Security Manager Replacement] Phase off SecurityManager usage in favor of Java Agent ([#17861](https://github.com/opensearch-project/OpenSearch/pull/17861)) - Support AutoExpand for SearchReplica ([#17741](https://github.com/opensearch-project/OpenSearch/pull/17741)) +- Add TLS enabled SecureNetty4GrpcServerTransport ([#17796](https://github.com/opensearch-project/OpenSearch/pull/17796)) - Implement fixed interval refresh task scheduling ([#17777](https://github.com/opensearch-project/OpenSearch/pull/17777)) - [Tiered caching] Create a single cache manager for all the disk caches. ([#17513](https://github.com/opensearch-project/OpenSearch/pull/17513)) - Add GRPC DocumentService and Bulk endpoint ([#17727](https://github.com/opensearch-project/OpenSearch/pull/17727)) diff --git a/plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/bootstrap/FlightService.java b/plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/bootstrap/FlightService.java index 7735fc3df73e0..fdcbbf43d75bf 100644 --- a/plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/bootstrap/FlightService.java +++ b/plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/bootstrap/FlightService.java @@ -134,6 +134,7 @@ public StreamManager getStreamManager() { * Retrieves the bound address of the FlightService. * @return The BoundTransportAddress instance. */ + @Override public BoundTransportAddress getBoundAddress() { return serverComponents.getBoundAddress(); } diff --git a/plugins/transport-grpc/README.md b/plugins/transport-grpc/README.md new file mode 100644 index 0000000000000..59c9bc94205b5 --- /dev/null +++ b/plugins/transport-grpc/README.md @@ -0,0 +1,42 @@ +# transport-grpc + +An auxiliary transport which runs in parallel to the REST API. +The `transport-grpc` plugin initializes a new client/server transport implementing a gRPC protocol on Netty4. + +Enable this transport with: + +``` +setting 'aux.transport.types', '[experimental-transport-grpc]' +setting 'aux.transport.experimental-transport-grpc.port', '9400-9500' //optional +``` + +For the secure transport: + +``` +setting 'aux.transport.types', '[experimental-secure-transport-grpc]' +setting 'aux.transport.experimental-secure-transport-grpc.port', '9400-9500' //optional +``` + +Other settings are agnostic as to the gRPC transport type: + +``` +setting 'grpc.publish_port', '9400' +setting 'grpc.host', '["0.0.0.0"]' +setting 'grpc.bind_host', '["0.0.0.0", "::", "10.0.0.1"]' +setting 'grpc.publish_host', '["thisnode.example.com"]' +setting 'grpc.netty.worker_count', '2' +``` + +## Testing + +### Unit Tests + +``` +./gradlew :plugins:transport-grpc:test +``` + +### Integration Tests + +``` +./gradlew :plugins:transport-grpc:internalClusterTest +``` diff --git a/plugins/transport-grpc/build.gradle b/plugins/transport-grpc/build.gradle index 3beed0ddc1bb0..12cbf0ecf76cf 100644 --- a/plugins/transport-grpc/build.gradle +++ b/plugins/transport-grpc/build.gradle @@ -1,5 +1,3 @@ -import org.gradle.api.attributes.java.TargetJvmEnvironment - /* * SPDX-License-Identifier: Apache-2.0 * @@ -8,6 +6,7 @@ import org.gradle.api.attributes.java.TargetJvmEnvironment * compatible open source license. */ +apply plugin: 'opensearch.testclusters' apply plugin: 'opensearch.internal-cluster-test' opensearchplugin { @@ -15,6 +14,13 @@ opensearchplugin { classname = 'org.opensearch.plugin.transport.grpc.GrpcPlugin' } +testClusters { + integTest { + plugin(project.path) + setting 'aux.transport.types', '[experimental-transport-grpc]' + } +} + dependencies { compileOnly "com.google.code.findbugs:jsr305:3.0.2" runtimeOnly "com.google.guava:guava:${versions.guava}" @@ -30,6 +36,7 @@ dependencies { implementation "io.grpc:grpc-util:${versions.grpc}" implementation "io.perfmark:perfmark-api:0.26.0" implementation "org.opensearch:protobufs:0.2.0" + testImplementation project(':test:framework') } tasks.named("dependencyLicenses").configure { diff --git a/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/GrpcTransportIT.java b/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportIT.java similarity index 52% rename from plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/GrpcTransportIT.java rename to plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportIT.java index a5e40c16b323e..48925feeb4464 100644 --- a/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/GrpcTransportIT.java +++ b/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportIT.java @@ -8,35 +8,45 @@ package org.opensearch.plugin.transport.grpc; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.cluster.health.ClusterHealthStatus; import org.opensearch.common.network.NetworkAddress; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.plugin.transport.grpc.ssl.NettyGrpcClient; import org.opensearch.plugins.Plugin; import org.opensearch.test.OpenSearchIntegTestCase; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; + +import io.grpc.health.v1.HealthCheckResponse; import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.GRPC_TRANSPORT_SETTING_KEY; -import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_PORT; import static org.opensearch.plugins.NetworkPlugin.AuxTransport.AUX_TRANSPORT_TYPES_KEY; -@OpenSearchIntegTestCase.ClusterScope(scope = OpenSearchIntegTestCase.Scope.TEST, numDataNodes = 2) -public class GrpcTransportIT extends OpenSearchIntegTestCase { +public class Netty4GrpcServerTransportIT extends OpenSearchIntegTestCase { - @Override - protected Collection> nodePlugins() { - return Collections.singletonList(GrpcPlugin.class); + private TransportAddress randomNetty4GrpcServerTransportAddr() { + List addresses = new ArrayList<>(); + for (Netty4GrpcServerTransport transport : internalCluster().getInstances(Netty4GrpcServerTransport.class)) { + TransportAddress tAddr = new TransportAddress(transport.getBoundAddress().publishAddress().address()); + addresses.add(tAddr); + } + return randomFrom(addresses); } @Override protected Settings nodeSettings(int nodeOrdinal) { - return Settings.builder() - .put(super.nodeSettings(nodeOrdinal)) - .put(SETTING_GRPC_PORT.getKey(), "0") - .put(AUX_TRANSPORT_TYPES_KEY, GRPC_TRANSPORT_SETTING_KEY) - .build(); + return Settings.builder().put(super.nodeSettings(nodeOrdinal)).put(AUX_TRANSPORT_TYPES_KEY, GRPC_TRANSPORT_SETTING_KEY).build(); + } + + @Override + protected Collection> nodePlugins() { + return Collections.singleton(GrpcPlugin.class); } public void testGrpcTransportStarted() { @@ -46,7 +56,7 @@ public void testGrpcTransportStarted() { assertNotNull("gRPC transport should be started on node " + nodeName, transport); // Verify that the transport is bound to an address - TransportAddress[] boundAddresses = transport.boundAddress().boundAddresses(); + TransportAddress[] boundAddresses = transport.getBoundAddress().boundAddresses(); assertTrue("gRPC transport should be bound to at least one address", boundAddresses.length > 0); // Log the bound addresses for debugging @@ -56,4 +66,15 @@ public void testGrpcTransportStarted() { } } } + + public void testStartGrpcTransportClusterHealth() throws Exception { + // REST api cluster health + ClusterHealthResponse healthResponse = client().admin().cluster().prepareHealth().get(); + assertEquals(ClusterHealthStatus.GREEN, healthResponse.getStatus()); + + // gRPC transport service health + try (NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(randomNetty4GrpcServerTransportAddr()).build()) { + assertEquals(client.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + } + } } diff --git a/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportIT.java b/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportIT.java new file mode 100644 index 0000000000000..0027e29e8c239 --- /dev/null +++ b/plugins/transport-grpc/src/internalClusterTest/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportIT.java @@ -0,0 +1,223 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.transport.grpc.ssl; + +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.cluster.health.ClusterHealthStatus; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.plugin.transport.grpc.GrpcPlugin; +import org.opensearch.plugins.NetworkPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SecureAuxTransportSettingsProvider; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.test.OpenSearchIntegTestCase; +import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import io.grpc.health.v1.HealthCheckResponse; + +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthNone; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthOptional; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthRequired; +import static org.opensearch.plugins.NetworkPlugin.AuxTransport.AUX_TRANSPORT_TYPES_KEY; +import static org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport.GRPC_SECURE_TRANSPORT_SETTING_KEY; + +public abstract class SecureNetty4GrpcServerTransportIT extends OpenSearchIntegTestCase { + + public static class MockSecurityPlugin extends Plugin implements NetworkPlugin { + public MockSecurityPlugin() {} + + static class MockSecureSettingsFactory implements SecureSettingsFactory { + @Override + public Optional getSecureTransportSettingsProvider(Settings settings) { + return Optional.empty(); + } + + @Override + public Optional getSecureHttpTransportSettingsProvider(Settings settings) { + return Optional.empty(); + } + + @Override + public Optional getSecureAuxTransportSettingsProvider(Settings settings) { + return Optional.empty(); + } + } + } + + protected TransportAddress randomNetty4GrpcServerTransportAddr() { + List addresses = new ArrayList<>(); + for (SecureNetty4GrpcServerTransport transport : internalCluster().getInstances(SecureNetty4GrpcServerTransport.class)) { + TransportAddress tAddr = new TransportAddress(transport.getBoundAddress().publishAddress().address()); + addresses.add(tAddr); + } + return randomFrom(addresses); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + .put(AUX_TRANSPORT_TYPES_KEY, GRPC_SECURE_TRANSPORT_SETTING_KEY) + .build(); + } + + @Override + protected Collection> nodePlugins() { + return List.of(GrpcPlugin.class, MockSecurityPlugin.class); + } + + private SecureSettingsHelpers.ConnectExceptions tryConnectClient(NettyGrpcClient client) { + try { + HealthCheckResponse.ServingStatus status = client.checkHealth(); + if (status == HealthCheckResponse.ServingStatus.SERVING) { + return SecureSettingsHelpers.ConnectExceptions.NONE; + } else { + throw new RuntimeException("Illegal state - unexpected server status: " + status.toString()); + } + } catch (Exception e) { + return SecureSettingsHelpers.ConnectExceptions.get(e); + } + } + + protected SecureSettingsHelpers.ConnectExceptions plaintextClientConnect() throws Exception { + try (NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(randomNetty4GrpcServerTransportAddr()).build()) { + return tryConnectClient(client); + } + } + + protected SecureSettingsHelpers.ConnectExceptions insecureClientConnect() throws Exception { + try ( + NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(randomNetty4GrpcServerTransportAddr()).insecure(true).build() + ) { + return tryConnectClient(client); + } + } + + protected SecureSettingsHelpers.ConnectExceptions trustedCertClientConnect() throws Exception { + try ( + NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(randomNetty4GrpcServerTransportAddr()) + .clientAuth(true) + .build() + ) { + return tryConnectClient(client); + } + } + + public void testClusterHealth() { + ClusterHealthResponse healthResponse = client().admin().cluster().prepareHealth().get(); + assertEquals(ClusterHealthStatus.GREEN, healthResponse.getStatus()); + } + + public static class SecureNetty4GrpcServerTransportNoAuthIT extends SecureNetty4GrpcServerTransportIT { + public static class NoAuthMockSecurityPlugin extends MockSecurityPlugin { + public NoAuthMockSecurityPlugin() {} + + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new MockSecureSettingsFactory() { + @Override + public Optional getSecureAuxTransportSettingsProvider(Settings settings) { + return Optional.of(getServerClientAuthNone()); + } + }); + } + } + + @Override + protected Collection> nodePlugins() { + return List.of(GrpcPlugin.class, NoAuthMockSecurityPlugin.class); + } + + public void testPlaintextClientConnect() throws Exception { + assertEquals(plaintextClientConnect(), SecureSettingsHelpers.ConnectExceptions.UNAVAILABLE); + } + + public void testInsecureClientConnect() throws Exception { + assertEquals(insecureClientConnect(), SecureSettingsHelpers.ConnectExceptions.NONE); + } + + public void testTrustedClientConnect() throws Exception { + assertEquals(trustedCertClientConnect(), SecureSettingsHelpers.ConnectExceptions.NONE); + } + } + + public static class SecureNetty4GrpcServerTransportOptionalAuthIT extends SecureNetty4GrpcServerTransportIT { + public static class OptAuthMockSecurityPlugin extends MockSecurityPlugin { + public OptAuthMockSecurityPlugin() {} + + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new MockSecureSettingsFactory() { + @Override + public Optional getSecureAuxTransportSettingsProvider(Settings settings) { + return Optional.of(getServerClientAuthOptional()); + } + }); + } + } + + @Override + protected Collection> nodePlugins() { + return List.of(GrpcPlugin.class, OptAuthMockSecurityPlugin.class); + } + + public void testPlaintextClientConnect() throws Exception { + assertEquals(plaintextClientConnect(), SecureSettingsHelpers.ConnectExceptions.UNAVAILABLE); + } + + public void testInsecureClientConnect() throws Exception { + assertEquals(insecureClientConnect(), SecureSettingsHelpers.ConnectExceptions.NONE); + } + + public void testTrustedClientConnect() throws Exception { + assertEquals(trustedCertClientConnect(), SecureSettingsHelpers.ConnectExceptions.NONE); + } + } + + public static class SecureNetty4GrpcServerTransportRequiredAuthIT extends SecureNetty4GrpcServerTransportIT { + public static class RequireAuthMockSecurityPlugin extends MockSecurityPlugin { + public RequireAuthMockSecurityPlugin() {} + + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new MockSecureSettingsFactory() { + @Override + public Optional getSecureAuxTransportSettingsProvider(Settings settings) { + return Optional.of(getServerClientAuthRequired()); + } + }); + } + } + + @Override + protected Collection> nodePlugins() { + return List.of(GrpcPlugin.class, RequireAuthMockSecurityPlugin.class); + } + + public void testPlaintextClientConnect() throws Exception { + assertEquals(plaintextClientConnect(), SecureSettingsHelpers.ConnectExceptions.UNAVAILABLE); + } + + public void testInsecureClientConnect() throws Exception { + assertEquals(insecureClientConnect(), SecureSettingsHelpers.ConnectExceptions.BAD_CERT); + } + + public void testTrustedClientConnect() throws Exception { + assertEquals(trustedCertClientConnect(), SecureSettingsHelpers.ConnectExceptions.NONE); + } + } +} diff --git a/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/GrpcPlugin.java b/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/GrpcPlugin.java index 26e9721da4f44..d552e56b0f71b 100644 --- a/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/GrpcPlugin.java +++ b/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/GrpcPlugin.java @@ -22,11 +22,13 @@ import org.opensearch.plugin.transport.grpc.services.SearchServiceImpl; import org.opensearch.plugins.NetworkPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SecureAuxTransportSettingsProvider; import org.opensearch.repositories.RepositoriesService; import org.opensearch.script.ScriptService; import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; +import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; import org.opensearch.watcher.ResourceWatcherService; import java.util.Collection; @@ -44,6 +46,8 @@ import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_PUBLISH_HOST; import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_PUBLISH_PORT; import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_WORKER_COUNT; +import static org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport.GRPC_SECURE_TRANSPORT_SETTING_KEY; +import static org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport.SETTING_GRPC_SECURE_PORT; /** * Main class for the gRPC plugin. @@ -88,13 +92,47 @@ public Map> getAuxTransports( ); } + /** + * Provides secure auxiliary transports for the plugin. + * Registered under a distinct key from gRPC transport. + * Consumes pluggable security settings as provided by a SecureAuxTransportSettingsProvider. + * + * @param settings The node settings + * @param threadPool The thread pool + * @param circuitBreakerService The circuit breaker service + * @param networkService The network service + * @param clusterSettings The cluster settings + * @param tracer The tracer + * @param secureAuxTransportSettingsProvider provides ssl context params + * @return A map of transport names to transport suppliers + */ + @Override + public Map> getSecureAuxTransports( + Settings settings, + ThreadPool threadPool, + CircuitBreakerService circuitBreakerService, + NetworkService networkService, + ClusterSettings clusterSettings, + SecureAuxTransportSettingsProvider secureAuxTransportSettingsProvider, + Tracer tracer + ) { + if (client == null) { + throw new RuntimeException("client cannot be null"); + } + List grpcServices = registerGRPCServices(new DocumentServiceImpl(client), new SearchServiceImpl(client)); + return Collections.singletonMap( + GRPC_SECURE_TRANSPORT_SETTING_KEY, + () -> new SecureNetty4GrpcServerTransport(settings, grpcServices, networkService, secureAuxTransportSettingsProvider) + ); + } + /** * Registers gRPC services to be exposed by the transport. * * @param services The gRPC services to register * @return A list of registered bindable services */ - protected List registerGRPCServices(BindableService... services) { + private List registerGRPCServices(BindableService... services) { return List.of(services); } @@ -107,6 +145,7 @@ protected List registerGRPCServices(BindableService... services public List> getSettings() { return List.of( SETTING_GRPC_PORT, + SETTING_GRPC_SECURE_PORT, SETTING_GRPC_HOST, SETTING_GRPC_PUBLISH_HOST, SETTING_GRPC_BIND_HOST, diff --git a/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransport.java b/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransport.java index 622834401970e..3107f762603f5 100644 --- a/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransport.java +++ b/plugins/transport-grpc/src/main/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransport.java @@ -32,9 +32,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.function.UnaryOperator; import io.grpc.BindableService; -import io.grpc.InsecureServerCredentials; import io.grpc.Server; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup; @@ -115,14 +115,29 @@ public class Netty4GrpcServerTransport extends NetworkPlugin.AuxTransport { Setting.Property.NodeScope ); - private final Settings settings; + /** + * Port range on which servers bind. + */ + protected PortsRange port; + + /** + * Port settings are set using the transport type, in this case GRPC_TRANSPORT_SETTING_KEY. + * Child classes have distinct transport type keys and need to override these settings. + */ + protected String portSettingKey; + + /** + * Settings. + */ + protected final Settings settings; + private final NetworkService networkService; private final List services; - private final CopyOnWriteArrayList servers = new CopyOnWriteArrayList<>(); private final String[] bindHosts; private final String[] publishHosts; - private final PortsRange port; private final int nettyEventLoopThreads; + private final CopyOnWriteArrayList servers = new CopyOnWriteArrayList<>(); + private final List> serverBuilderConfigs = new ArrayList<>(); private volatile BoundTransportAddress boundAddress; private volatile EventLoopGroup eventLoopGroup; @@ -150,12 +165,23 @@ public Netty4GrpcServerTransport(Settings settings, List servic this.port = SETTING_GRPC_PORT.get(settings); this.nettyEventLoopThreads = SETTING_GRPC_WORKER_COUNT.get(settings); + this.portSettingKey = SETTING_GRPC_PORT.getKey(); } - BoundTransportAddress boundAddress() { + // public for tests + @Override + public BoundTransportAddress getBoundAddress() { return this.boundAddress; } + /** + * Inject a NettyServerBuilder configuration to be applied at server bind and start. + * @param configModifier builder configuration to set. + */ + protected void addServerConfig(UnaryOperator configModifier) { + serverBuilderConfigs.add(configModifier); + } + /** * Starts the gRPC server transport. * Initializes the event loop group and binds the server to the configured addresses. @@ -210,7 +236,7 @@ protected void doStop() { */ @Override protected void doClose() { - + eventLoopGroup.close(); } private void bindServer() { @@ -242,7 +268,7 @@ private void bindServer() { + publishInetAddress + "). " + "Please specify a unique port by setting " - + SETTING_GRPC_PORT.getKey() + + portSettingKey + " or " + SETTING_GRPC_PUBLISH_PORT.getKey() ); @@ -261,13 +287,18 @@ private TransportAddress bindAddress(InetAddress hostAddress, PortsRange portRan try { final InetSocketAddress address = new InetSocketAddress(hostAddress, portNumber); - final NettyServerBuilder serverBuilder = NettyServerBuilder.forAddress(address, InsecureServerCredentials.create()) + final NettyServerBuilder serverBuilder = NettyServerBuilder.forAddress(address) + .directExecutor() .bossEventLoopGroup(eventLoopGroup) .workerEventLoopGroup(eventLoopGroup) .channelType(NioServerSocketChannel.class) .addService(new HealthStatusManager().getHealthService()) .addService(ProtoReflectionService.newInstance()); + for (UnaryOperator op : serverBuilderConfigs) { + op.apply(serverBuilder); + } + services.forEach(serverBuilder::addService); Server srv = serverBuilder.build().start(); diff --git a/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/SecureNetty4GrpcServerTransport.java b/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/SecureNetty4GrpcServerTransport.java new file mode 100644 index 0000000000000..14e7fc2d8b227 --- /dev/null +++ b/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/SecureNetty4GrpcServerTransport.java @@ -0,0 +1,130 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.transport.grpc.ssl; + +import org.opensearch.common.network.NetworkService; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.transport.PortsRange; +import org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport; +import org.opensearch.plugins.SecureAuxTransportSettingsProvider; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; + +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import io.grpc.BindableService; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolNames; +import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth; +import io.grpc.netty.shaded.io.netty.handler.ssl.JdkSslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SupportedCipherSuiteFilter; + +/** + * Netty4GrpcServerTransport with TLS enabled. + * Security settings injected through a SecureAuxTransportSettingsProvider. + */ +public class SecureNetty4GrpcServerTransport extends Netty4GrpcServerTransport { + private static final String[] DEFAULT_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + + /** + * Type key to select secure transport. + */ + public static final String GRPC_SECURE_TRANSPORT_SETTING_KEY = "experimental-secure-transport-grpc"; + + /** + * Distinct port setting required as it depends on transport type key. + */ + public static final Setting SETTING_GRPC_SECURE_PORT = AUX_TRANSPORT_PORT.getConcreteSettingForNamespace( + GRPC_SECURE_TRANSPORT_SETTING_KEY + ); + + /** + * In the case no SecureAuxTransportParameters restrict client auth mode to REQUIRE. + * Assume no enabled cipher suites. Allow ssl context implementation to select defaults. + */ + private static class DefaultParameters implements SecureAuxTransportSettingsProvider.SecureAuxTransportParameters { + @Override + public Optional clientAuth() { + return Optional.of(ClientAuth.REQUIRE.name()); + } + + @Override + public Collection cipherSuites() { + return List.of(); + } + } + + /** + * Creates a new SecureNetty4GrpcServerTransport instance and inject a SecureAuxTransportSslContext + * into the NettyServerBuilder config to enable TLS on the server. + * @param settings the configured settings. + * @param services the gRPC compatible services to be registered with the server. + * @param networkService the bind/publish addresses. + * @param secureTransportSettingsProvider TLS configuration settings. + */ + public SecureNetty4GrpcServerTransport( + Settings settings, + List services, + NetworkService networkService, + SecureAuxTransportSettingsProvider secureTransportSettingsProvider + ) { + super(settings, services, networkService); + this.port = SecureNetty4GrpcServerTransport.SETTING_GRPC_SECURE_PORT.get(settings); + this.portSettingKey = SecureNetty4GrpcServerTransport.SETTING_GRPC_SECURE_PORT.getKey(); + try { + JdkSslContext ctxt = getSslContext(settings, secureTransportSettingsProvider); + this.addServerConfig((NettyServerBuilder builder) -> builder.sslContext(ctxt)); + } catch (Exception e) { + throw new RuntimeException("Failed to build SslContext for " + SecureNetty4GrpcServerTransport.class.getName(), e); + } + } + + /** + * Construct JdkSslContext, wrapping javax SSLContext as supplied by SecureAuxTransportSettingsProvider with applied + * configurations settings in SecureAuxTransportParameters for this transport. + * If optional SSLContext is empty, use default context as configured through JDK. + * If SecureAuxTransportParameters empty, set ClientAuth OPTIONAL and allow all default supported ciphers. + * @param settings the configured settings. + * @param provider for SSLContext and SecureAuxTransportParameters (ClientAuth and enabled ciphers). + */ + private JdkSslContext getSslContext(Settings settings, SecureAuxTransportSettingsProvider provider) throws SSLException { + Optional sslContext = provider.buildSecureAuxServerTransportContext(settings, this); + if (sslContext.isEmpty()) { + try { + sslContext = Optional.of(SSLContext.getDefault()); + } catch (NoSuchAlgorithmException e) { + throw new SSLException("Failed to build default SSLContext for " + SecureNetty4GrpcServerTransport.class.getName(), e); + } + } + SecureAuxTransportSettingsProvider.SecureAuxTransportParameters params = provider.parameters().orElseGet(DefaultParameters::new); + ClientAuth clientAuth = ClientAuth.valueOf(params.clientAuth().orElseThrow().toUpperCase(Locale.ROOT)); + return new JdkSslContext( + sslContext.get(), + false, + params.cipherSuites(), + SupportedCipherSuiteFilter.INSTANCE, + new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2 // gRPC -> always http2 + ), + clientAuth, + DEFAULT_SSL_PROTOCOLS, + true + ); + } +} diff --git a/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/package-info.java b/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/package-info.java new file mode 100644 index 0000000000000..bffc3e762a0f4 --- /dev/null +++ b/plugins/transport-grpc/src/main/java/org/opensearch/transport/grpc/ssl/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * gRPC transport for OpenSearch implementing TLS. + */ +package org.opensearch.transport.grpc.ssl; diff --git a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/GrpcPluginTests.java b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/GrpcPluginTests.java index 974602bce3278..31e6d9e25715c 100644 --- a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/GrpcPluginTests.java +++ b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/GrpcPluginTests.java @@ -18,6 +18,7 @@ import org.opensearch.test.OpenSearchTestCase; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; +import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; import org.junit.Before; import java.util.List; @@ -34,6 +35,9 @@ import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_PUBLISH_HOST; import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_PUBLISH_PORT; import static org.opensearch.plugin.transport.grpc.Netty4GrpcServerTransport.SETTING_GRPC_WORKER_COUNT; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthNone; +import static org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport.GRPC_SECURE_TRANSPORT_SETTING_KEY; +import static org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport.SETTING_GRPC_SECURE_PORT; public class GrpcPluginTests extends OpenSearchTestCase { @@ -87,6 +91,7 @@ public void testGetSettings() { // Verify that all expected settings are returned assertTrue("SETTING_GRPC_PORT should be included", settings.contains(SETTING_GRPC_PORT)); + assertTrue("SETTING_GRPC_SECURE_PORT should be included", settings.contains(SETTING_GRPC_SECURE_PORT)); assertTrue("SETTING_GRPC_HOST should be included", settings.contains(SETTING_GRPC_HOST)); assertTrue("SETTING_GRPC_PUBLISH_HOST should be included", settings.contains(SETTING_GRPC_PUBLISH_HOST)); assertTrue("SETTING_GRPC_BIND_HOST should be included", settings.contains(SETTING_GRPC_BIND_HOST)); @@ -94,7 +99,7 @@ public void testGetSettings() { assertTrue("SETTING_GRPC_PUBLISH_PORT should be included", settings.contains(SETTING_GRPC_PUBLISH_PORT)); // Verify the number of settings - assertEquals("Should return 6 settings", 6, settings.size()); + assertEquals("Should return 7 settings", 7, settings.size()); } public void testGetAuxTransports() { @@ -116,4 +121,25 @@ public void testGetAuxTransports() { NetworkPlugin.AuxTransport transport = transports.get(GRPC_TRANSPORT_SETTING_KEY).get(); assertTrue("Should return a Netty4GrpcServerTransport instance", transport instanceof Netty4GrpcServerTransport); } + + public void testGetSecureAuxTransports() { + Settings settings = Settings.builder().put(SETTING_GRPC_SECURE_PORT.getKey(), "9200-9300").build(); + + Map> transports = plugin.getSecureAuxTransports( + settings, + threadPool, + circuitBreakerService, + networkService, + clusterSettings, + getServerClientAuthNone(), + tracer + ); + + // Verify that the transport map contains the expected key + assertTrue("Should contain GRPC_SECURE_TRANSPORT_SETTING_KEY", transports.containsKey(GRPC_SECURE_TRANSPORT_SETTING_KEY)); + + // Verify that the supplier returns a Netty4GrpcServerTransport instance + NetworkPlugin.AuxTransport transport = transports.get(GRPC_SECURE_TRANSPORT_SETTING_KEY).get(); + assertTrue("Should return a SecureNetty4GrpcServerTransport instance", transport instanceof SecureNetty4GrpcServerTransport); + } } diff --git a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportTests.java b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportTests.java index dcade2e8bf880..198b92dad672c 100644 --- a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportTests.java +++ b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/Netty4GrpcServerTransportTests.java @@ -12,6 +12,7 @@ import org.opensearch.common.network.NetworkService; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.plugin.transport.grpc.ssl.NettyGrpcClient; import org.opensearch.test.OpenSearchTestCase; import org.hamcrest.MatcherAssert; import org.junit.Before; @@ -19,6 +20,7 @@ import java.util.List; import io.grpc.BindableService; +import io.grpc.health.v1.HealthCheckResponse; import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.not; @@ -38,13 +40,39 @@ public void testBasicStartAndStop() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(createSettings(), services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - assertNotNull(transport.boundAddress().publishAddress().address()); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + assertNotNull(transport.getBoundAddress().publishAddress().address()); transport.stop(); } } + public void testGrpcTransportHealthcheck() { + try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(createSettings(), services, networkService)) { + transport.start(); + final TransportAddress remoteAddress = randomFrom(transport.getBoundAddress().boundAddresses()); + try (NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(remoteAddress).build()) { + assertEquals(client.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + } + transport.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void testGrpcTransportListServices() { + try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(createSettings(), services, networkService)) { + transport.start(); + final TransportAddress remoteAddress = randomFrom(transport.getBoundAddress().boundAddresses()); + try (NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(remoteAddress).build()) { + assertTrue(client.listServices().get().size() > 1); + } + transport.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public void testWithCustomPort() { // Create settings with a specific port Settings settings = Settings.builder().put(Netty4GrpcServerTransport.SETTING_GRPC_PORT.getKey(), "9000-9010").build(); @@ -52,8 +80,8 @@ public void testWithCustomPort() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - TransportAddress publishAddress = transport.boundAddress().publishAddress(); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + TransportAddress publishAddress = transport.getBoundAddress().publishAddress(); assertNotNull(publishAddress.address()); assertTrue("Port should be in the specified range", publishAddress.getPort() >= 9000 && publishAddress.getPort() <= 9010); @@ -71,8 +99,8 @@ public void testWithCustomPublishPort() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - TransportAddress publishAddress = transport.boundAddress().publishAddress(); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + TransportAddress publishAddress = transport.getBoundAddress().publishAddress(); assertNotNull(publishAddress.address()); assertEquals("Publish port should match the specified value", 9000, publishAddress.getPort()); @@ -90,8 +118,8 @@ public void testWithCustomHost() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - TransportAddress publishAddress = transport.boundAddress().publishAddress(); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + TransportAddress publishAddress = transport.getBoundAddress().publishAddress(); assertNotNull(publishAddress.address()); assertEquals( "Host should match the specified value", @@ -113,8 +141,8 @@ public void testWithCustomBindHost() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - TransportAddress boundAddress = transport.boundAddress().boundAddresses()[0]; + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + TransportAddress boundAddress = transport.getBoundAddress().boundAddresses()[0]; assertNotNull(boundAddress.address()); assertEquals( "Bind host should match the specified value", @@ -136,8 +164,8 @@ public void testWithCustomPublishHost() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - TransportAddress publishAddress = transport.boundAddress().publishAddress(); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + TransportAddress publishAddress = transport.getBoundAddress().publishAddress(); assertNotNull(publishAddress.address()); assertEquals( "Publish host should match the specified value", @@ -159,8 +187,8 @@ public void testWithCustomWorkerCount() { try (Netty4GrpcServerTransport transport = new Netty4GrpcServerTransport(settings, services, networkService)) { transport.start(); - MatcherAssert.assertThat(transport.boundAddress().boundAddresses(), not(emptyArray())); - assertNotNull(transport.boundAddress().publishAddress().address()); + MatcherAssert.assertThat(transport.getBoundAddress().boundAddresses(), not(emptyArray())); + assertNotNull(transport.getBoundAddress().publishAddress().address()); transport.stop(); } diff --git a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/NettyGrpcClient.java b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/NettyGrpcClient.java new file mode 100644 index 0000000000000..21e94a96a8285 --- /dev/null +++ b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/NettyGrpcClient.java @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.transport.grpc.ssl; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.core.common.transport.TransportAddress; + +import javax.net.ssl.SSLException; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import io.grpc.ManagedChannel; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthCheckResponse; +import io.grpc.health.v1.HealthGrpc; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig; +import io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolNames; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.reflection.v1alpha.ServiceResponse; +import io.grpc.stub.StreamObserver; + +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.CLIENT_KEYSTORE; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getTestKeyManagerFactory; +import static io.grpc.internal.GrpcUtil.NOOP_PROXY_DETECTOR; + +public class NettyGrpcClient implements AutoCloseable { + private static final Logger logger = LogManager.getLogger(NettyGrpcClient.class); + private final ManagedChannel channel; + private final HealthGrpc.HealthBlockingStub healthStub; + private final ServerReflectionGrpc.ServerReflectionStub reflectionStub; + + public NettyGrpcClient(NettyChannelBuilder channelBuilder) { + channel = channelBuilder.build(); + healthStub = HealthGrpc.newBlockingStub(channel); + reflectionStub = ServerReflectionGrpc.newStub(channel); + } + + public void shutdown() throws InterruptedException { + channel.shutdown(); + if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { + channel.shutdownNow(); // forced shutdown + if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("Unable to shutdown the managed channel gracefully"); + } + } + } + + @Override + public void close() throws Exception { + shutdown(); + } + + /** + * List available gRPC services available on server. + * Note: ProtoReflectionService only implements a streaming interface and has no blocking stub. + * @return services registered on the server. + */ + public CompletableFuture> listServices() { + CompletableFuture> respServices = new CompletableFuture<>(); + + StreamObserver responseObserver = new StreamObserver<>() { + final List services = new ArrayList<>(); + + @Override + public void onNext(ServerReflectionResponse response) { + if (response.hasListServicesResponse()) { + services.addAll(response.getListServicesResponse().getServiceList()); + } + } + + @Override + public void onError(Throwable t) { + respServices.completeExceptionally(t); + throw new RuntimeException(t); + } + + @Override + public void onCompleted() { + respServices.complete(services); + } + }; + + StreamObserver requestObserver = reflectionStub.serverReflectionInfo(responseObserver); + requestObserver.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + requestObserver.onCompleted(); + return respServices; + } + + /** + * Request server status. + * @return HealthCheckResponse.ServingStatus. + */ + public HealthCheckResponse.ServingStatus checkHealth() { + return healthStub.check(HealthCheckRequest.newBuilder().build()).getStatus(); + } + + public static class Builder { + private Boolean clientAuth = false; + private Boolean insecure = false; + private TransportAddress addr; + + private static final ApplicationProtocolConfig CLIENT_ALPN = new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2 + ); + + public Builder() {} + + public NettyGrpcClient build() throws SSLException { + NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress(addr.getAddress(), addr.getPort()) + .proxyDetector(NOOP_PROXY_DETECTOR); + + if (clientAuth || insecure) { + SslContextBuilder builder = SslContextBuilder.forClient(); + builder.sslProvider(SslProvider.JDK); + builder.applicationProtocolConfig(CLIENT_ALPN); + if (clientAuth) { + builder.keyManager(getTestKeyManagerFactory(CLIENT_KEYSTORE)); + } + builder.trustManager(InsecureTrustManagerFactory.INSTANCE); + channelBuilder.sslContext(builder.build()); + } else { + channelBuilder.usePlaintext(); + } + + return new NettyGrpcClient(channelBuilder); + } + + public Builder setAddress(TransportAddress addr) { + this.addr = addr; + return this; + } + + /** + * Enable clientAuth - load client keystore. + */ + public Builder clientAuth(boolean enable) { + this.clientAuth = enable; + return this; + } + + /** + * Enable insecure TLS client. + */ + public Builder insecure(boolean enable) { + this.insecure = enable; + return this; + } + } +} diff --git a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportTests.java b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportTests.java new file mode 100644 index 0000000000000..1c841bf6f0d22 --- /dev/null +++ b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureNetty4GrpcServerTransportTests.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.transport.grpc.ssl; + +import org.opensearch.common.network.NetworkService; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; +import org.junit.After; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.grpc.BindableService; +import io.grpc.StatusRuntimeException; +import io.grpc.health.v1.HealthCheckResponse; + +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.ConnectExceptions.BAD_CERT; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthNone; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthOptional; +import static org.opensearch.plugin.transport.grpc.ssl.SecureSettingsHelpers.getServerClientAuthRequired; + +public class SecureNetty4GrpcServerTransportTests extends OpenSearchTestCase { + private NetworkService networkService; + private final List services = new ArrayList<>(); + + static Settings createSettings() { + return Settings.builder().put(SecureNetty4GrpcServerTransport.SETTING_GRPC_PORT.getKey(), getPortRange()).build(); + } + + @Before + public void setup() { + networkService = new NetworkService(Collections.emptyList()); + } + + @After + public void shutdown() { + networkService = null; + } + + public void testGrpcSecureTransportStartStop() { + try ( + SecureNetty4GrpcServerTransport transport = new SecureNetty4GrpcServerTransport( + createSettings(), + services, + networkService, + getServerClientAuthNone() + ) + ) { + transport.start(); + assertTrue(transport.getBoundAddress().boundAddresses().length > 0); + assertNotNull(transport.getBoundAddress().publishAddress().address()); + transport.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void testGrpcInsecureAuthTLS() { + try ( + SecureNetty4GrpcServerTransport transport = new SecureNetty4GrpcServerTransport( + createSettings(), + services, + networkService, + getServerClientAuthNone() + ) + ) { + transport.start(); + assertTrue(transport.getBoundAddress().boundAddresses().length > 0); + assertNotNull(transport.getBoundAddress().publishAddress().address()); + final TransportAddress remoteAddress = randomFrom(transport.getBoundAddress().boundAddresses()); + + // Client without cert + NettyGrpcClient client = new NettyGrpcClient.Builder().setAddress(remoteAddress).insecure(true).build(); + assertEquals(client.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + client.close(); + + transport.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void testGrpcOptionalAuthTLS() { + try ( + SecureNetty4GrpcServerTransport transport = new SecureNetty4GrpcServerTransport( + createSettings(), + services, + networkService, + getServerClientAuthOptional() + ) + ) { + transport.start(); + assertTrue(transport.getBoundAddress().boundAddresses().length > 0); + assertNotNull(transport.getBoundAddress().publishAddress().address()); + final TransportAddress remoteAddress = randomFrom(transport.getBoundAddress().boundAddresses()); + + // Client without cert + NettyGrpcClient hasNoCertClient = new NettyGrpcClient.Builder().setAddress(remoteAddress).insecure(true).build(); + assertEquals(hasNoCertClient.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + hasNoCertClient.close(); + + // Client with trusted cert + NettyGrpcClient hasTrustedCertClient = new NettyGrpcClient.Builder().setAddress(remoteAddress).clientAuth(true).build(); + assertEquals(hasTrustedCertClient.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + hasTrustedCertClient.close(); + + transport.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void testGrpcRequiredAuthTLS() { + try ( + SecureNetty4GrpcServerTransport transport = new SecureNetty4GrpcServerTransport( + createSettings(), + services, + networkService, + getServerClientAuthRequired() + ) + ) { + transport.start(); + assertTrue(transport.getBoundAddress().boundAddresses().length > 0); + assertNotNull(transport.getBoundAddress().publishAddress().address()); + final TransportAddress remoteAddress = randomFrom(transport.getBoundAddress().boundAddresses()); + + // Client without cert + NettyGrpcClient hasNoCertClient = new NettyGrpcClient.Builder().setAddress(remoteAddress).insecure(true).build(); + assertThrows(StatusRuntimeException.class, hasNoCertClient::checkHealth); + try { + hasNoCertClient.checkHealth(); + } catch (Exception e) { + assertEquals(SecureSettingsHelpers.ConnectExceptions.get(e), BAD_CERT); + } + hasNoCertClient.close(); + + // Client with trusted cert + NettyGrpcClient hasTrustedCertClient = new NettyGrpcClient.Builder().setAddress(remoteAddress).clientAuth(true).build(); + assertEquals(hasTrustedCertClient.checkHealth(), HealthCheckResponse.ServingStatus.SERVING); + hasTrustedCertClient.close(); + + transport.stop(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} diff --git a/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureSettingsHelpers.java b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureSettingsHelpers.java new file mode 100644 index 0000000000000..955194ae7e5f1 --- /dev/null +++ b/plugins/transport-grpc/src/test/java/org/opensearch/plugin/transport/grpc/ssl/SecureSettingsHelpers.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugin.transport.grpc.ssl; + +import org.opensearch.common.settings.Settings; +import org.opensearch.plugins.NetworkPlugin; +import org.opensearch.plugins.SecureAuxTransportSettingsProvider; +import org.opensearch.transport.grpc.ssl.SecureNetty4GrpcServerTransport; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import static org.opensearch.test.OpenSearchTestCase.randomFrom; + +public class SecureSettingsHelpers { + private static final String TEST_PASS = "password"; // used for all keystores + static final String SERVER_KEYSTORE = "/netty4-server-secure.jks"; + static final String CLIENT_KEYSTORE = "/netty4-client-secure.jks"; + static final String[] DEFAULT_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + static final String[] DEFAULT_CIPHERS = { + "TLS_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" }; + + /** + * Exception messages for various types of TLS client/server connection failure. + * We would like to check to ensure a connection fails in the way we expect. + * However, depending on the default JDK provider exceptions may differ slightly, + * so we allow a couple different error messages for each possible error. + */ + public enum ConnectExceptions { + NONE(List.of("Connection succeeded")), + UNAVAILABLE(List.of("Network closed for unknown reason")), + BAD_CERT(List.of("bad_certificate", "certificate_required")); + + List msgList = null; + + ConnectExceptions(List exceptionMsg) { + this.msgList = exceptionMsg; + } + + public static ConnectExceptions get(Throwable e) { + if (e.getMessage() != null) { + for (ConnectExceptions exception : values()) { + if (exception == NONE) continue; // Skip success message + if (exception.msgList.stream().anyMatch(substring -> e.getMessage().contains(substring))) { + return exception; + } + } + } + if (e.getCause() != null) { + return get(e.getCause()); + } + throw new RuntimeException("Unexpected exception", e); + } + } + + public static KeyManagerFactory getTestKeyManagerFactory(String keystorePath) { + KeyManagerFactory keyManagerFactory; + try { + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(SecureNetty4GrpcServerTransport.class.getResourceAsStream(keystorePath), TEST_PASS.toCharArray()); + keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, TEST_PASS.toCharArray()); + } catch (UnrecoverableKeyException | CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return keyManagerFactory; + } + + static TrustManagerFactory getTestTrustManagerFactory(String keystorePath) { + try { + final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(SecureNetty4GrpcServerTransport.class.getResourceAsStream(keystorePath), TEST_PASS.toCharArray()); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + return trustManagerFactory; + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { + throw new RuntimeException(e); + } + } + + static SecureAuxTransportSettingsProvider getSecureSettingsProvider( + String clientAuth, + KeyManagerFactory keyMngerFactory, + TrustManagerFactory trustMngerFactory + ) { + return new SecureAuxTransportSettingsProvider() { + @Override + public Optional buildSecureAuxServerTransportContext(Settings settings, NetworkPlugin.AuxTransport transport) + throws SSLException { + // Choose a random protocol from among supported test defaults + String protocol = randomFrom(DEFAULT_SSL_PROTOCOLS); + // Default JDK provider + SSLContext testContext; + try { + testContext = SSLContext.getInstance(protocol); + testContext.init(keyMngerFactory.getKeyManagers(), trustMngerFactory.getTrustManagers(), new SecureRandom()); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new SSLException("Failed to build mock provider", e); + } + return Optional.of(testContext); + } + + @Override + public Optional parameters() { + return Optional.of(new SecureAuxTransportParameters() { + @Override + public Optional clientAuth() { + return Optional.of(clientAuth); + } + + @Override + public Collection cipherSuites() { + return List.of(DEFAULT_CIPHERS); + } + }); + } + }; + } + + public static SecureAuxTransportSettingsProvider getServerClientAuthRequired() { + return getSecureSettingsProvider( + ClientAuth.REQUIRE.name().toUpperCase(Locale.ROOT), + getTestKeyManagerFactory(SERVER_KEYSTORE), + getTestTrustManagerFactory(CLIENT_KEYSTORE) + ); + } + + public static SecureAuxTransportSettingsProvider getServerClientAuthOptional() { + return getSecureSettingsProvider( + ClientAuth.OPTIONAL.name().toUpperCase(Locale.ROOT), + getTestKeyManagerFactory(SERVER_KEYSTORE), + getTestTrustManagerFactory(CLIENT_KEYSTORE) + ); + } + + public static SecureAuxTransportSettingsProvider getServerClientAuthNone() { + return getSecureSettingsProvider( + ClientAuth.NONE.name().toUpperCase(Locale.ROOT), + getTestKeyManagerFactory(SERVER_KEYSTORE), + InsecureTrustManagerFactory.INSTANCE + ); + } +} diff --git a/plugins/transport-grpc/src/test/resources/README.txt b/plugins/transport-grpc/src/test/resources/README.txt new file mode 100644 index 0000000000000..d2315aea07404 --- /dev/null +++ b/plugins/transport-grpc/src/test/resources/README.txt @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# This is README describes how the certificates in this directory were created. +# This file can also be executed as a script +# + +# 1. Create server & client certificate key + +openssl req -x509 -sha256 -newkey rsa:2048 -keyout server.key -out server.crt -days 8192 -nodes +openssl req -x509 -sha256 -newkey rsa:2048 -keyout client.key -out client.crt -days 8192 -nodes + +# 2. Export the certificates in pkcs12 format + +openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 -name netty4-server-secure -password pass:password +openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name netty4-client-secure -password pass:password + +# 3. Import the certificate into JDK keystore (PKCS12 type) + +keytool -importkeystore -srcstorepass password -destkeystore netty4-server-secure.jks -srckeystore server.p12 -srcstoretype PKCS12 -alias netty4-server-secure -deststorepass password +keytool -importkeystore -srcstorepass password -destkeystore netty4-client-secure.jks -srckeystore client.p12 -srcstoretype PKCS12 -alias netty4-client-secure -deststorepass password + +# 4. Clean up - Clean up pkcs12 keystores and private keys +rm client.key +rm client.p12 +rm server.key +rm server.p12 diff --git a/plugins/transport-grpc/src/test/resources/netty4-client-secure.jks b/plugins/transport-grpc/src/test/resources/netty4-client-secure.jks new file mode 100644 index 0000000000000000000000000000000000000000..3497de56fc956710a82778710bff9995eb25042d GIT binary patch literal 2772 zcma);c{CJk8^&jt8H^dbA{m3MS!V2%W$a5NX^PNTChJ(z$QsR9LSsvkb&!4Q%@)}v zM%GZM5TWc@k|f*fJKuMD&-d5&o##B~x$oa~U-zHac~B(i5Cf1AMS=!HSfrzkqj%sy zCLomr^#qZiuE*FJMFRK!mjr$ZB7qx@an13qhOqv}1%m<^s3fq?F+7jD`NsizX7zO0XdL4tz;h(y64%>UlVzyb!K1R*TAXk#Fr z5d@S5ais2LofSdAJZP403<7Mod_W}7&}9#48!iw`SSzz`(zZ;R3CJkOPIB3G--RqBKz-SyUs#TSj8XPVl=eO; zjO(mwoK(=!Uy3Omv#&Awd7T$|#>?_|gEj;IUVR;6Kgf;5u z!oJKoebBpj=~Zu>8+F7cWoJ>tr9+U$sa1TH&Ob1+{|hb-%=I<*V5;&Cu8F91XKXnS zb&e67YP`jCUGS#3R6~W1xYcFezBn=Z3A+CZbTr!LW2|vM4Z{hY4_C=|r>a?1Pks&N z6>G8H*Q~0$K|V(X4YO0*D|BByxpPf_p;M^EI=RY>ytoAUKy#!#)%um=j;3{ME6&VF zp8~_k%I$q6CknAT=gZVC)g#mJmE~C+`MuM&$l`5gLs?8KCb%_8Ja2Mw9N`sT@NT9s zI>C59wO^p#d`;?v%S>2k=Pu3`xUw9QGBmquHm72O4(Dr;*yc0&0iR_>KW7t+3bYrS z+dk)=#gOg$oBgjIgXEq5AF`Mmb$uf3O0iLm!eJG10#{Avat8LY6C2lxxX`}S_ZjbU z%!nC71Vsr|awN+#phyvRw`sX|28qR!4bKVQ)n-lSDB+4Bm{P+vT) z`c#AXj2e~fC{NrNu;O<59FR;as{92Hw8A&FbWI<`>Zs5!fA+cPaO&|3h+SlIVnLJb z;lnm2AI+|nT>H7jm06K&bFxmn01%LYRh%lR+yti99#T_2E;bd=e)E3zkdmB>6`4PX z5ZJeVp4TE!=I(&%`U~+X6gh1ypfz1bW)fxBsK>ZyOiJ7GjWHk=%ytAJnUnMg>dh?z zskx{Y=XyBH7Av~|ys4E3004+T)t>3$5~gBCm7F+fI16ichcu*!%7CG2yVb zq{)I^Zk?2hQ>1vxb+(*AJyWd8TQ!SB9zI-l{rhg6pKmZ)k7`d^G;Ocu_Z9zyv8k6# zkp0$KuCaxreGYkCet3@|*OSuU>87!QbIlEgwM2LNJ$oDkq;cN`r8rJxO#CDcf! zY60851@{Tgh#npLgRTc-Ep0}e9{}h#&3dm2&fEN0kikZnS0B|BKeNJsm1**itBo~w z*WUyTS=dKCbCT^V(e7{~`nTPeNA675H^0>X`kf`6EfURat@IN|O)X2H4`Cg{xh)HC zF?z`~s`aV5Yia0_R`&%nB2Ee|WgxW}znvHQ5ODv=ING!7oWLEca3S(xlP*b@7FqN1 zO3^3_JwwikSw-xG;Pc8WNj<9HgNHpog=1>;E#NgBPR6I;=|DfB+-Y;1+>5vq79FX= zVT#9aA+aP_Bv}U zF`sO z)t+bTR<=jbb|}?9amm7imPSYdya9N??c);!Py)yToB>_{_haIHOk@H6$CWdH2)G5n zqlEu4@}hY_T-NRcJQA&{q=-VPDXJ=~DJY{zpyWS8Ob{vwL^?)y8GwM}=gvP0@P9@4 zcoSlOTV4+v^|3r9U^AZ(@f!!`g#ET|$a*9TfK!C+QP?*d%Wt@p% zhlgC-nU|4rdRkrU>iNYYk|i7^E3yvM=Wak4-QfJdACu7}Exa_}eseOKdtUuq(-idM zp8m=VTdS{&8QVJFvVp-+^y%-0p)cx(l#+s*D_d^Yp1h!in2NBkF1Kekkqdj9mkQik zt*s=n8%LjQ7GBRMRD9u1LA*kgh@D z&L#H>iGh0byumyop^kRv30$w!Xh2x$+f^Q5i^bF^$OQ2${9>o|q=;Tm`tCWER#ry` zBCJu}X9+-V!72=fm`cv~D}Bnk@HWv_9@0PNVlI}fq>s!T1S7S1rgqw2~GdJ^q%9X?X-HFrD zH}c(S;p^QT4*a2YvG0kh+uCRK=+wAKWv@?lA~p>(ki(h_4SDQ@E9z34b-qp{KCk!E z^D%Tu(X9!NVla7o)fAWIz4qAYc(K_~(vdR&?zLrkvl=h`#h zy7=*NA^`z;Tu~29qX|ZDOT6*j?$0Gkj1hN34zQz*D~b($oYPBBiYiOT)tI@sUe7`_ zx%{E=Lcig%fhGhQ`pW^mi*5H_KY|iZb({=AZb5G+@^arBb4ZJnHiImyL=5g7G0!PI zI`_46_Q_8nHw&=0UmH2sKAlC7XaoJRq8solcUfS8BhXl)o1<(5;-bJ_T1Rpbg={|R zGFr{Fh5G)=>4EH$!gLA^zr}M#RcJ6JPKC}(5p4Y5F^jF~C#e&%0#K2qYPYKqmn|P0 zl#JV;M|n#}xt*nAOv^30myDR!hBo^QupKPwv>-*hw{NVSc0`s)%Z{2EIvMTw+lgNL z-XH)(e&gVNl)+U>EfGR|I-BMp#=5d!>r~UtL)=;Swh*x01N@W-Md#KBUGl7vMYZ6o zF}C72VG6hUJJNdg$Qi=uK9ZnRyTa0c|GHrnb*3iO3t)HemGdUQla}IZ)4_;>t=e! zZ$*u8qU=NieJMsGCBNXoVzJ%w!)(UWrQJTf3jHNtZsfsJU@J0JuCLOD;}&}-HZmN6 zr43T0zjP#PmQPc7G@1)i&GM&ACKB{g9;3`rXcYXw9 yZ+>sN@$N+?`*rn!n}YWoIy5?xKs@5Qg|d>nJRP$WgY|plTkQeS<{tlwpuYjS`{Ypo literal 0 HcmV?d00001 diff --git a/plugins/transport-grpc/src/test/resources/netty4-server-secure.jks b/plugins/transport-grpc/src/test/resources/netty4-server-secure.jks new file mode 100644 index 0000000000000000000000000000000000000000..5e7d09ded52d076756d67c486dcc3f928b4e4695 GIT binary patch literal 2772 zcma);XHXN`7KKyky%+>S4^jkz^rDD`CLn4kp(+uiOA8T*lz>PC>C&Wxpb+U&M0zKY zUX&szEg&EuhzN@1^4@*po%id#IdjgL_3g9vzrE%l2`nQZAQ(wt31MKCi!qAXX9qF@ zNdy*82!Z9+5yl`1(7yjFL7N~1X#EkcIog#Btp9Pbu>e6N0`%+=)J3}faX{ITXr$&p zk_{;j;getbu?>CPW=X~tM$WqVJbW}D9R>oO-~s`~kZcT0e|H2iLjg!Q1M~G5BcKZy z0+fSrHLVHr8VMa|DdZIGp4c*}f)F4h6^l}+v`>PtHOjIrs;i1Rg4-oF0A7=YalYO7k|YyUw4fibNm1mB2?l0q@HyKoQlcPY47Wu zIZ2l!dVd<2m8wM*Kki6Y@vxvPxR6q$!DOtOD+;9q3N4jNG-3@$&crZUo$H4ILQEc`|!VTP^FoYx)r zb`13F-9xBBik9V$EYHsehT~Jy;n5_+E+YL_Pi}Ri6W3(4SclMIf?{sDn&gNy+Q9IX zR`j}wHCN^##RWe5_+mJ##app$TdL{&JRF#vytnpCseCrTkm9$$sEKZB@e)M!mn;dBUa)Ya0~-SAcP zMpK8xv&S{VA;aWZ5zaa#D&-o$xdtzBkr5d$J%~($p(Em+E#C+c2UqLFEKO?rAJ@3W z@g`E9F3@$WvO~ZqO$hvD_vy6ww)5fCKFtnyv0pCk1Ppnrd;`FzA1pYP9G_Hn{=&Dg zwZYKPJP?FMAyEp}JO*rjCU?~|+gz`V|NWhP58Fv)L4!?J&iebr)$uk0!q4Db)a(rN z{Byj_cb)`Je~z)~W`;{z3UduR+-LjMJ76x#uUjd$cha}MerhTgfzP%023FkTE`jHLC#&~Scns+Ed zt@DP*wHS9f4}pb`1v_hNBaLOH4w4NdtL3ys<8p{rKda2<)4-(cLKc4jU+C)9-Y%WS zl1Uwb%;qs3!d!DZ|C?`jK5K@~!-o-d=DWoZDhMxO(`tW{$vIj53`M%_VfpW!%?!QF zJZ(cZJKlZKqPe1Pn$UY~J}DPY49l7fBxNPN{wR7CPM8lWm}mxup5P|$Rpz3Y7VK6>U%hKdSLAWr5TgyrayfZ z{M2w(!)sb~RnOfSGL1DubhU0yj}O`8%5>a=*A+qyX!pm#y9f# zW6&$_D#xF2a7Ow$*nMdO6Zkgs1K^w@CK*pmR_F`%zwyjl8`tDVXB!FYYx}QQWl|fp zjDNvm^f8yXGFWMtCz=;V4y-y&Ya<&xwfoYY_?|w`;V}IEvkZaM_ z_#d&T?ke*cWqYK?pSWb^QWVD#|B3LDreHKVm*^ya_U3Me#}$JJ0t6tg*Z8K`>L*G!Euujv z0~z1%U6~9WK4dQ@`Op6f8Y&2@GDsxm99!{Sq^DDu9E&n;xZN$1O7gOl&QsHFKJg^oOak^nO2W+5W|A_adm;0 z@r!nKCSNwDGJ>h`?9Fvuyn*D_t)i7Gmcpf!xp&sDO@AbBW+$c#{vqCdJlEeK zBU{F4C{=RRwwZRhe)9G6F5IYKy_7<@9sHOk8sed{>^O#7Eo-`q%a?6Az&vi3q(KnN zKGim)MB7?V%gC)=GhsFOjRzAp3MPuZJ()0ob;YC2UXkH0Ylw6TlT1sSLPCj4$yavq z^X@~Hm?fb!YCw^&Q5~Zx+_8Y9ZIYE*m^vz1bKN)!U*NAyH^gGB>G%%IQyYioRio4J z!dD4AZgxK?0TDwl1|qMr$5I!=H}ADeM+NNOx6yq7mGa%);#>05v88h1nVS=|*PN3LzO)wjN!=+mHWxjPN`lt3C&&dqA z?F`6yxr%zf{+w9PA=yP|oiinl?csCe@mt=kW1lZionBMW7uIDag>ccPRF1zHq( secureAuxTransportSettingsProviders = secureSettingsFactories.stream() + .map(p -> p.getSecureAuxTransportSettingsProvider(settings)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + if (secureAuxTransportSettingsProviders.size() > 1) { + throw new IllegalArgumentException( + "there is more than one secure auxiliary transport settings provider: " + secureAuxTransportSettingsProviders + ); + } + for (NetworkPlugin plugin : plugins) { Map> httpTransportFactory = plugin.getHttpTransports( settings, @@ -274,6 +287,24 @@ public NetworkModule( } } + // Register any secure auxiliary transports if available + if (secureAuxTransportSettingsProviders.isEmpty() == false) { + final SecureAuxTransportSettingsProvider secureSettingProvider = secureAuxTransportSettingsProviders.iterator().next(); + + final Map> secureAuxTransportFactory = plugin.getSecureAuxTransports( + settings, + threadPool, + circuitBreakerService, + networkService, + clusterSettings, + secureSettingProvider, + tracer + ); + for (Map.Entry> entry : secureAuxTransportFactory.entrySet()) { + registerAuxTransport(entry.getKey(), entry.getValue()); + } + } + // Register any secure transports if available if (secureTransportSettingsProviders.isEmpty() == false) { final SecureTransportSettingsProvider secureSettingProvider = secureTransportSettingsProviders.iterator().next(); diff --git a/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java b/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java index 4442189373c93..b294c64e5cdce 100644 --- a/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java +++ b/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java @@ -42,6 +42,7 @@ import org.opensearch.common.util.PageCacheRecycler; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.common.transport.BoundTransportAddress; import org.opensearch.core.indices.breaker.CircuitBreakerService; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.http.HttpServerTransport; @@ -75,6 +76,7 @@ public interface NetworkPlugin { * bootstrap. To allow pluggable AuxTransports access to configurable port ranges we require the port range be provided * through an {@link org.opensearch.common.settings.Setting.AffixSetting} of the form 'AUX_SETTINGS_PREFIX.{aux-transport-key}.ports'. */ + @ExperimentalApi abstract class AuxTransport extends AbstractLifecycleComponent { public static final String AUX_SETTINGS_PREFIX = "aux.transport."; public static final String AUX_TRANSPORT_TYPES_KEY = AUX_SETTINGS_PREFIX + "types"; @@ -91,6 +93,9 @@ abstract class AuxTransport extends AbstractLifecycleComponent { Function.identity(), Setting.Property.NodeScope ); + + // public for tests + public abstract BoundTransportAddress getBoundAddress(); } /** @@ -159,6 +164,23 @@ default Map> getHttpTransports( return Collections.emptyMap(); } + /** + * Returns a map of secure {@link AuxTransport} suppliers. + * See {@link org.opensearch.plugins.NetworkPlugin.AuxTransport#AUX_TRANSPORT_TYPES_SETTING} to configure a specific implementation. + */ + @ExperimentalApi + default Map> getSecureAuxTransports( + Settings settings, + ThreadPool threadPool, + CircuitBreakerService circuitBreakerService, + NetworkService networkService, + ClusterSettings clusterSettings, + SecureAuxTransportSettingsProvider secureAuxTransportSettingsProvider, + Tracer tracer + ) { + return Collections.emptyMap(); + } + /** * Returns a map of secure {@link Transport} suppliers. * See {@link org.opensearch.common.network.NetworkModule#TRANSPORT_TYPE_KEY} to configure a specific implementation. diff --git a/server/src/main/java/org/opensearch/plugins/SecureAuxTransportSettingsProvider.java b/server/src/main/java/org/opensearch/plugins/SecureAuxTransportSettingsProvider.java new file mode 100644 index 0000000000000..6274807f12149 --- /dev/null +++ b/server/src/main/java/org/opensearch/plugins/SecureAuxTransportSettingsProvider.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugins; + +import org.opensearch.common.annotation.ExperimentalApi; +import org.opensearch.common.settings.Settings; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; + +import java.util.Collection; +import java.util.Optional; + +/** + * A security settings provider for auxiliary transports. + * @opensearch.experimental + */ +@ExperimentalApi +public interface SecureAuxTransportSettingsProvider { + /** + * Fetch an SSLContext as managed by pluggable security provider. + * @return an instance of SSLContext. + */ + default Optional buildSecureAuxServerTransportContext(Settings settings, NetworkPlugin.AuxTransport transport) + throws SSLException { + return Optional.empty(); + } + + /** + * Additional params required for configuring ALPN. + * @return an instance of {@link SecureAuxTransportSettingsProvider.SecureAuxTransportParameters} + */ + default Optional parameters() { + return Optional.empty(); + } + + /** + * ALPN configuration parameters. + */ + @ExperimentalApi + interface SecureAuxTransportParameters { + Optional clientAuth(); + + Collection cipherSuites(); + } +} diff --git a/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java b/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java index ec2276ecc62ef..0fdf4b6927eb0 100644 --- a/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java +++ b/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java @@ -33,4 +33,11 @@ public interface SecureSettingsFactory { * @return optionally, the instance of the {@link SecureHttpTransportSettingsProvider} */ Optional getSecureHttpTransportSettingsProvider(Settings settings); + + /** + * Creates (or provides pre-created) instance of the {@link SecureAuxTransportSettingsProvider} + * @param settings settings + * @return optionally, the instance of the {@link SecureAuxTransportSettingsProvider} + */ + Optional getSecureAuxTransportSettingsProvider(Settings settings); } diff --git a/server/src/main/java/org/opensearch/transport/TransportAdapterProvider.java b/server/src/main/java/org/opensearch/transport/TransportAdapterProvider.java index 36dbd5a699b40..7e39445b1699c 100644 --- a/server/src/main/java/org/opensearch/transport/TransportAdapterProvider.java +++ b/server/src/main/java/org/opensearch/transport/TransportAdapterProvider.java @@ -32,7 +32,7 @@ public interface TransportAdapterProvider { * Provides a new transport adapter of required transport adapter class and transport instance. * @param transport adapter class * @param settings settings - * @param transport HTTP transport instance + * @param transport transport instance * @param adapterClass required transport adapter class * @return the non-empty {@link Optional} if the transport adapter could be created, empty one otherwise */ diff --git a/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java b/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java index 447377e372e61..c07fa0e183c00 100644 --- a/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java +++ b/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java @@ -47,6 +47,7 @@ import org.opensearch.http.HttpStats; import org.opensearch.http.NullDispatcher; import org.opensearch.plugins.NetworkPlugin; +import org.opensearch.plugins.SecureAuxTransportSettingsProvider; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; @@ -130,6 +131,12 @@ public Optional buildHttpServerExceptionHandler( } }); } + + @Override + public Optional getSecureAuxTransportSettingsProvider(Settings settings) { + return Optional.of(new SecureAuxTransportSettingsProvider() { + }); + } }; }