From e7983d8d79d3451fe3124a33b763fa8006596ec2 Mon Sep 17 00:00:00 2001
From: Andriy Redko <andriy.redko@aiven.io>
Date: Wed, 20 Mar 2024 10:01:59 -0400
Subject: [PATCH] [FEATURE] Built-in secure transports support (#12435)

* [FEATURE] Built-in secure transports support

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

* Added more tests, addressing code review comments

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

* Address code review comments

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

* Address code review comments

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

* Address code review comments

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

---------

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>
(cherry picked from commit 773a93945429d99e3551fad6e4746668778aa563)
---
 CHANGELOG.md                                  |   1 +
 .../ssl/SecureNetty4HttpServerTransport.java  | 126 ++++
 .../opensearch/transport/Netty4Plugin.java    |  64 ++
 .../netty4/ssl/DualModeSslHandler.java        | 106 +++
 .../netty4/ssl/SecureConnectionTestUtil.java  | 214 +++++++
 .../netty4/ssl/SecureNetty4Transport.java     | 316 +++++++++
 .../transport/netty4/ssl/SslUtils.java        | 107 ++++
 .../http/netty4/Netty4HttpClient.java         |  35 +-
 .../SecureNetty4HttpServerTransportTests.java | 603 ++++++++++++++++++
 .../ssl/SimpleSecureNetty4TransportTests.java | 234 +++++++
 .../transport/netty4/ssl/TrustAllManager.java |  28 +
 .../src/test/resources/README.txt             |  17 +
 .../src/test/resources/certificate.crt        |  22 +
 .../src/test/resources/certificate.key        |  28 +
 .../src/test/resources/netty4-secure.jks      | Bin 0 -> 2790 bytes
 .../common/network/NetworkModule.java         |  69 +-
 .../common/settings/ClusterSettings.java      |   3 +
 .../main/java/org/opensearch/node/Node.java   |  11 +-
 .../org/opensearch/plugins/NetworkPlugin.java |  37 ++
 .../java/org/opensearch/plugins/Plugin.java   |   9 +
 .../plugins/SecureSettingsFactory.java        |  29 +
 .../SecureTransportSettingsProvider.java      |  90 +++
 .../common/network/NetworkModuleTests.java    |  95 ++-
 23 files changed, 2237 insertions(+), 7 deletions(-)
 create mode 100644 modules/transport-netty4/src/main/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransport.java
 create mode 100644 modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/DualModeSslHandler.java
 create mode 100644 modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureConnectionTestUtil.java
 create mode 100644 modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureNetty4Transport.java
 create mode 100644 modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SslUtils.java
 create mode 100644 modules/transport-netty4/src/test/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransportTests.java
 create mode 100644 modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/SimpleSecureNetty4TransportTests.java
 create mode 100644 modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/TrustAllManager.java
 create mode 100644 modules/transport-netty4/src/test/resources/README.txt
 create mode 100644 modules/transport-netty4/src/test/resources/certificate.crt
 create mode 100644 modules/transport-netty4/src/test/resources/certificate.key
 create mode 100644 modules/transport-netty4/src/test/resources/netty4-secure.jks
 create mode 100644 server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java
 create mode 100644 server/src/main/java/org/opensearch/plugins/SecureTransportSettingsProvider.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 705ab4487c10c..2b30aa93b8e0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - [Admission Control] Integrated IO Based AdmissionController to AdmissionControl Framework ([#12583](https://github.com/opensearch-project/OpenSearch/pull/12583))
 - Add Remote Store Migration Experimental flag and allow mixed mode clusters under same ([#11986](https://github.com/opensearch-project/OpenSearch/pull/11986))
 - Introduce a new setting `index.check_pending_flush.enabled` to expose the ability to disable the check for pending flushes by write threads ([#12710](https://github.com/opensearch-project/OpenSearch/pull/12710))
+- Built-in secure transports support ([#12435](https://github.com/opensearch-project/OpenSearch/pull/12435))
 
 ### Dependencies
 - Bump `com.squareup.okio:okio` from 3.7.0 to 3.8.0 ([#12290](https://github.com/opensearch-project/OpenSearch/pull/12290))
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransport.java b/modules/transport-netty4/src/main/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransport.java
new file mode 100644
index 0000000000000..39cdffefa3c63
--- /dev/null
+++ b/modules/transport-netty4/src/main/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransport.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2015-2017 floragunn GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * 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.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+package org.opensearch.http.netty4.ssl;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.opensearch.common.network.NetworkService;
+import org.opensearch.common.settings.ClusterSettings;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.common.util.BigArrays;
+import org.opensearch.core.xcontent.NamedXContentRegistry;
+import org.opensearch.http.HttpChannel;
+import org.opensearch.http.HttpHandlingSettings;
+import org.opensearch.http.netty4.Netty4HttpServerTransport;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
+import org.opensearch.telemetry.tracing.Tracer;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.SharedGroupFactory;
+import org.opensearch.transport.netty4.ssl.SslUtils;
+
+import javax.net.ssl.SSLEngine;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.handler.codec.DecoderException;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * @see <a href="https://github.com/opensearch-project/security/blob/d526c9f6c2a438c14db8b413148204510b9fe2e2/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java">SecuritySSLNettyHttpServerTransport</a>
+ */
+public class SecureNetty4HttpServerTransport extends Netty4HttpServerTransport {
+    private static final Logger logger = LogManager.getLogger(SecureNetty4HttpServerTransport.class);
+    private final SecureTransportSettingsProvider secureTransportSettingsProvider;
+    private final SecureTransportSettingsProvider.ServerExceptionHandler exceptionHandler;
+
+    public SecureNetty4HttpServerTransport(
+        final Settings settings,
+        final NetworkService networkService,
+        final BigArrays bigArrays,
+        final ThreadPool threadPool,
+        final NamedXContentRegistry namedXContentRegistry,
+        final Dispatcher dispatcher,
+        final ClusterSettings clusterSettings,
+        final SharedGroupFactory sharedGroupFactory,
+        final SecureTransportSettingsProvider secureTransportSettingsProvider,
+        final Tracer tracer
+    ) {
+        super(
+            settings,
+            networkService,
+            bigArrays,
+            threadPool,
+            namedXContentRegistry,
+            dispatcher,
+            clusterSettings,
+            sharedGroupFactory,
+            tracer
+        );
+        this.secureTransportSettingsProvider = secureTransportSettingsProvider;
+        this.exceptionHandler = secureTransportSettingsProvider.buildHttpServerExceptionHandler(settings, this)
+            .orElse(SecureTransportSettingsProvider.ServerExceptionHandler.NOOP);
+    }
+
+    @Override
+    public ChannelHandler configureServerChannelHandler() {
+        return new SslHttpChannelHandler(this, handlingSettings);
+    }
+
+    @Override
+    public void onException(HttpChannel channel, Exception cause0) {
+        Throwable cause = cause0;
+
+        if (cause0 instanceof DecoderException && cause0 != null) {
+            cause = cause0.getCause();
+        }
+
+        exceptionHandler.onError(cause);
+        logger.error("Exception during establishing a SSL connection: " + cause, cause);
+        super.onException(channel, cause0);
+    }
+
+    protected class SslHttpChannelHandler extends Netty4HttpServerTransport.HttpChannelHandler {
+        protected SslHttpChannelHandler(final Netty4HttpServerTransport transport, final HttpHandlingSettings handlingSettings) {
+            super(transport, handlingSettings);
+        }
+
+        @Override
+        protected void initChannel(Channel ch) throws Exception {
+            super.initChannel(ch);
+
+            final SSLEngine sslEngine = secureTransportSettingsProvider.buildSecureHttpServerEngine(
+                settings,
+                SecureNetty4HttpServerTransport.this
+            ).orElseGet(SslUtils::createDefaultServerSSLEngine);
+
+            final SslHandler sslHandler = new SslHandler(sslEngine);
+            ch.pipeline().addFirst("ssl_http", sslHandler);
+        }
+    }
+}
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/transport/Netty4Plugin.java b/modules/transport-netty4/src/main/java/org/opensearch/transport/Netty4Plugin.java
index 4258fa8e04d61..3a760afd14514 100644
--- a/modules/transport-netty4/src/main/java/org/opensearch/transport/Netty4Plugin.java
+++ b/modules/transport-netty4/src/main/java/org/opensearch/transport/Netty4Plugin.java
@@ -46,11 +46,14 @@
 import org.opensearch.core.xcontent.NamedXContentRegistry;
 import org.opensearch.http.HttpServerTransport;
 import org.opensearch.http.netty4.Netty4HttpServerTransport;
+import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport;
 import org.opensearch.plugins.NetworkPlugin;
 import org.opensearch.plugins.Plugin;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
 import org.opensearch.telemetry.tracing.Tracer;
 import org.opensearch.threadpool.ThreadPool;
 import org.opensearch.transport.netty4.Netty4Transport;
+import org.opensearch.transport.netty4.ssl.SecureNetty4Transport;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -61,7 +64,9 @@
 public class Netty4Plugin extends Plugin implements NetworkPlugin {
 
     public static final String NETTY_TRANSPORT_NAME = "netty4";
+    public static final String NETTY_SECURE_TRANSPORT_NAME = "netty4-secure";
     public static final String NETTY_HTTP_TRANSPORT_NAME = "netty4";
+    public static final String NETTY_SECURE_HTTP_TRANSPORT_NAME = "netty4-secure";
 
     private final SetOnce<SharedGroupFactory> groupFactory = new SetOnce<>();
 
@@ -144,6 +149,65 @@ public Map<String, Supplier<HttpServerTransport>> getHttpTransports(
         );
     }
 
+    @Override
+    public Map<String, Supplier<HttpServerTransport>> getSecureHttpTransports(
+        Settings settings,
+        ThreadPool threadPool,
+        BigArrays bigArrays,
+        PageCacheRecycler pageCacheRecycler,
+        CircuitBreakerService circuitBreakerService,
+        NamedXContentRegistry xContentRegistry,
+        NetworkService networkService,
+        HttpServerTransport.Dispatcher dispatcher,
+        ClusterSettings clusterSettings,
+        SecureTransportSettingsProvider secureTransportSettingsProvider,
+        Tracer tracer
+    ) {
+        return Collections.singletonMap(
+            NETTY_SECURE_HTTP_TRANSPORT_NAME,
+            () -> new SecureNetty4HttpServerTransport(
+                settings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry,
+                dispatcher,
+                clusterSettings,
+                getSharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                tracer
+            )
+        );
+    }
+
+    @Override
+    public Map<String, Supplier<Transport>> getSecureTransports(
+        Settings settings,
+        ThreadPool threadPool,
+        PageCacheRecycler pageCacheRecycler,
+        CircuitBreakerService circuitBreakerService,
+        NamedWriteableRegistry namedWriteableRegistry,
+        NetworkService networkService,
+        SecureTransportSettingsProvider secureTransportSettingsProvider,
+        Tracer tracer
+    ) {
+        return Collections.singletonMap(
+            NETTY_SECURE_TRANSPORT_NAME,
+            () -> new SecureNetty4Transport(
+                settings,
+                Version.CURRENT,
+                threadPool,
+                networkService,
+                pageCacheRecycler,
+                namedWriteableRegistry,
+                circuitBreakerService,
+                getSharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                tracer
+            )
+        );
+    }
+
     SharedGroupFactory getSharedGroupFactory(Settings settings) {
         SharedGroupFactory groupFactory = this.groupFactory.get();
         if (groupFactory != null) {
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/DualModeSslHandler.java b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/DualModeSslHandler.java
new file mode 100644
index 0000000000000..1bf4cdb0eb438
--- /dev/null
+++ b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/DualModeSslHandler.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+package org.opensearch.transport.netty4.ssl;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
+import org.opensearch.transport.TcpTransport;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * Modifies the current pipeline dynamically to enable TLS
+ *
+ * @see <a href="https://github.com/opensearch-project/security/blob/d526c9f6c2a438c14db8b413148204510b9fe2e2/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java">DualModeSSLHandler</a>
+ */
+public class DualModeSslHandler extends ByteToMessageDecoder {
+
+    private static final Logger logger = LogManager.getLogger(DualModeSslHandler.class);
+    private final Settings settings;
+    private final SecureTransportSettingsProvider secureTransportSettingsProvider;
+    private final TcpTransport transport;
+    private final SslHandler providedSSLHandler;
+
+    public DualModeSslHandler(
+        final Settings settings,
+        final SecureTransportSettingsProvider secureTransportSettingsProvider,
+        final TcpTransport transport
+    ) {
+        this(settings, secureTransportSettingsProvider, transport, null);
+    }
+
+    protected DualModeSslHandler(
+        final Settings settings,
+        final SecureTransportSettingsProvider secureTransportSettingsProvider,
+        final TcpTransport transport,
+        SslHandler providedSSLHandler
+    ) {
+        this.settings = settings;
+        this.secureTransportSettingsProvider = secureTransportSettingsProvider;
+        this.transport = transport;
+        this.providedSSLHandler = providedSSLHandler;
+    }
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        // Will use the first six bytes to detect a protocol.
+        if (in.readableBytes() < 6) {
+            return;
+        }
+        int offset = in.readerIndex();
+        if (in.getCharSequence(offset, 6, StandardCharsets.UTF_8).equals(SecureConnectionTestUtil.DUAL_MODE_CLIENT_HELLO_MSG)) {
+            logger.debug("Received DualSSL Client Hello message");
+            ByteBuf responseBuffer = Unpooled.buffer(6);
+            responseBuffer.writeCharSequence(SecureConnectionTestUtil.DUAL_MODE_SERVER_HELLO_MSG, StandardCharsets.UTF_8);
+            ctx.writeAndFlush(responseBuffer).addListener(ChannelFutureListener.CLOSE);
+            return;
+        }
+
+        if (SslUtils.isTLS(in)) {
+            logger.debug("Identified request as SSL request");
+            enableSsl(ctx);
+        } else {
+            logger.debug("Identified request as non SSL request, running in HTTP mode as dual mode is enabled");
+            ctx.pipeline().remove(this);
+        }
+    }
+
+    private void enableSsl(ChannelHandlerContext ctx) throws SSLException, NoSuchAlgorithmException {
+        final SSLEngine sslEngine = secureTransportSettingsProvider.buildSecureServerTransportEngine(settings, transport)
+            .orElseGet(SslUtils::createDefaultServerSSLEngine);
+
+        SslHandler sslHandler;
+        if (providedSSLHandler != null) {
+            sslHandler = providedSSLHandler;
+        } else {
+            sslHandler = new SslHandler(sslEngine);
+        }
+        ChannelPipeline p = ctx.pipeline();
+        p.addAfter("port_unification_handler", "ssl_server", sslHandler);
+        p.remove(this);
+        logger.debug("Removed port unification handler and added SSL handler as incoming request is SSL");
+    }
+}
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureConnectionTestUtil.java b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureConnectionTestUtil.java
new file mode 100644
index 0000000000000..d5667475ea007
--- /dev/null
+++ b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureConnectionTestUtil.java
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+package org.opensearch.transport.netty4.ssl;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Utility class to test if the server supports SSL connections.
+ * SSL Check will be done by sending an OpenSearch Ping to see if server is replying to pings.
+ * Following that a custom client hello message will be sent to the server, if the server
+ * side has OpenSearchPortUnificationHandler it will reply with server hello message.
+ *
+ * @see <a href="https://github.com/opensearch-project/security/blob/d526c9f6c2a438c14db8b413148204510b9fe2e2/src/main/java/org/opensearch/security/ssl/util/SSLConnectionTestUtil.java">SSLConnectionTestUtil</a>
+ */
+class SecureConnectionTestUtil {
+    private static final Logger logger = LogManager.getLogger(SecureConnectionTestUtil.class);
+
+    /**
+     * Return codes for SSLConnectionTestUtil.testConnection()
+     */
+    enum SSLConnectionTestResult {
+        /**
+         * OpenSearch Ping to the server failed.
+         */
+        OPENSEARCH_PING_FAILED,
+        /**
+         * Server does not support SSL.
+         */
+        SSL_NOT_AVAILABLE,
+        /**
+         * Server supports SSL.
+         */
+        SSL_AVAILABLE
+    }
+
+    public static final byte[] OPENSEARCH_PING_MSG = new byte[] {
+        (byte) 'E',
+        (byte) 'S',
+        (byte) 0xFF,
+        (byte) 0xFF,
+        (byte) 0xFF,
+        (byte) 0xFF };
+    public static final String DUAL_MODE_CLIENT_HELLO_MSG = "DUALCM";
+    public static final String DUAL_MODE_SERVER_HELLO_MSG = "DUALSM";
+    private static final int SOCKET_TIMEOUT_MILLIS = 10 * 1000;
+    private final String host;
+    private final int port;
+    private Socket overriddenSocket = null;
+    private OutputStreamWriter testOutputStreamWriter = null;
+    private InputStreamReader testInputStreamReader = null;
+
+    public SecureConnectionTestUtil(final String host, final int port) {
+        this.host = host;
+        this.port = port;
+    }
+
+    protected SecureConnectionTestUtil(
+        final String host,
+        final int port,
+        final Socket overriddenSocket,
+        final OutputStreamWriter testOutputStreamWriter,
+        final InputStreamReader testInputStreamReader
+    ) {
+        this.overriddenSocket = overriddenSocket;
+        this.testOutputStreamWriter = testOutputStreamWriter;
+        this.testInputStreamReader = testInputStreamReader;
+
+        this.host = host;
+        this.port = port;
+    }
+
+    /**
+     * Test connection to server by performing the below steps:
+     * - Send Client Hello to check if the server replies with Server Hello which indicates that Server understands SSL
+     * - Send OpenSearch Ping to check if the server replies to the OpenSearch Ping message
+     *
+     * @return SSLConnectionTestResult i.e. OPENSEARCH_PING_FAILED or SSL_NOT_AVAILABLE or SSL_AVAILABLE
+     */
+    public SSLConnectionTestResult testConnection() {
+        if (sendDualSSLClientHello()) {
+            return SSLConnectionTestResult.SSL_AVAILABLE;
+        }
+
+        if (sendOpenSearchPing()) {
+            return SSLConnectionTestResult.SSL_NOT_AVAILABLE;
+        }
+
+        return SSLConnectionTestResult.OPENSEARCH_PING_FAILED;
+    }
+
+    private boolean sendDualSSLClientHello() {
+        boolean dualSslSupported = false;
+        Socket socket = null;
+        try {
+            OutputStreamWriter outputStreamWriter;
+            InputStreamReader inputStreamReader;
+            if (overriddenSocket != null) {
+                socket = overriddenSocket;
+                outputStreamWriter = testOutputStreamWriter;
+                inputStreamReader = testInputStreamReader;
+            } else {
+                socket = new Socket(InetAddress.getByName(host), port);
+                outputStreamWriter = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
+                inputStreamReader = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
+            }
+
+            socket.setSoTimeout(SOCKET_TIMEOUT_MILLIS);
+            outputStreamWriter.write(DUAL_MODE_CLIENT_HELLO_MSG);
+            outputStreamWriter.flush();
+            logger.debug("Sent DualSSL Client Hello msg to {}", host);
+
+            StringBuilder sb = new StringBuilder();
+            int currentChar;
+            while ((currentChar = inputStreamReader.read()) != -1) {
+                sb.append((char) currentChar);
+            }
+
+            if (sb.toString().equals(DUAL_MODE_SERVER_HELLO_MSG)) {
+                logger.debug("Received DualSSL Server Hello msg from {}", host);
+                dualSslSupported = true;
+            }
+        } catch (IOException e) {
+            logger.debug("DualSSL client check failed for {}, exception {}", host, e.getMessage());
+        } finally {
+            logger.debug("Closing DualSSL check client socket for {}", host);
+            if (socket != null) {
+                try {
+                    socket.close();
+                } catch (IOException e) {
+                    logger.error(
+                        "Exception occurred while closing DualSSL check client socket for {}. Exception: {}",
+                        host,
+                        e.getMessage()
+                    );
+                }
+            }
+        }
+        logger.debug("dualSslClient check with server {}, server supports ssl = {}", host, dualSslSupported);
+        return dualSslSupported;
+    }
+
+    private boolean sendOpenSearchPing() {
+        boolean pingSucceeded = false;
+        Socket socket = null;
+        try {
+            if (overriddenSocket != null) {
+                socket = overriddenSocket;
+            } else {
+                socket = new Socket(InetAddress.getByName(host), port);
+            }
+
+            socket.setSoTimeout(SOCKET_TIMEOUT_MILLIS);
+            OutputStream outputStream = socket.getOutputStream();
+            InputStream inputStream = socket.getInputStream();
+
+            logger.debug("Sending OpenSearch Ping to {}", host);
+            outputStream.write(OPENSEARCH_PING_MSG);
+            outputStream.flush();
+
+            int currentByte;
+            int byteBufIndex = 0;
+            byte[] response = new byte[6];
+            while ((byteBufIndex < 6) && ((currentByte = inputStream.read()) != -1)) {
+                response[byteBufIndex] = (byte) currentByte;
+                byteBufIndex++;
+            }
+            if (byteBufIndex == 6) {
+                logger.debug("Received reply for OpenSearch Ping. from {}", host);
+                pingSucceeded = true;
+                for (int i = 0; i < 6; i++) {
+                    if (response[i] != OPENSEARCH_PING_MSG[i]) {
+                        // Unexpected byte in response
+                        logger.error("Received unexpected byte in OpenSearch Ping reply from {}", host);
+                        pingSucceeded = false;
+                        break;
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            logger.error("OpenSearch Ping failed for {}, exception: {}", host, ex.getMessage());
+        } finally {
+            logger.debug("Closing OpenSearch Ping client socket for connection to {}", host);
+            if (socket != null) {
+                try {
+                    socket.close();
+                } catch (IOException e) {
+                    logger.error("Exception occurred while closing socket for {}. Exception: {}", host, e.getMessage());
+                }
+            }
+        }
+
+        logger.debug("OpenSearch Ping check to server {} result = {}", host, pingSucceeded);
+        return pingSucceeded;
+    }
+}
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureNetty4Transport.java b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureNetty4Transport.java
new file mode 100644
index 0000000000000..9c63a1ab9161b
--- /dev/null
+++ b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SecureNetty4Transport.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2015-2017 floragunn GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * 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.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+package org.opensearch.transport.netty4.ssl;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.opensearch.ExceptionsHelper;
+import org.opensearch.OpenSearchSecurityException;
+import org.opensearch.Version;
+import org.opensearch.cluster.node.DiscoveryNode;
+import org.opensearch.common.SuppressForbidden;
+import org.opensearch.common.network.NetworkModule;
+import org.opensearch.common.network.NetworkService;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.common.util.PageCacheRecycler;
+import org.opensearch.core.common.io.stream.NamedWriteableRegistry;
+import org.opensearch.core.indices.breaker.CircuitBreakerService;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
+import org.opensearch.telemetry.tracing.Tracer;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.SharedGroupFactory;
+import org.opensearch.transport.TcpChannel;
+import org.opensearch.transport.netty4.Netty4Transport;
+import org.opensearch.transport.netty4.ssl.SecureConnectionTestUtil.SSLConnectionTestResult;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.DecoderException;
+import io.netty.handler.ssl.SslHandler;
+
+/**
+ * @see <a href="https://github.com/opensearch-project/security/blob/d526c9f6c2a438c14db8b413148204510b9fe2e2/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java">SecuritySSLNettyTransport</a>
+ */
+public class SecureNetty4Transport extends Netty4Transport {
+
+    private static final Logger logger = LogManager.getLogger(SecureNetty4Transport.class);
+    private final SecureTransportSettingsProvider secureTransportSettingsProvider;
+    private final SecureTransportSettingsProvider.ServerExceptionHandler exceptionHandler;
+
+    public SecureNetty4Transport(
+        final Settings settings,
+        final Version version,
+        final ThreadPool threadPool,
+        final NetworkService networkService,
+        final PageCacheRecycler pageCacheRecycler,
+        final NamedWriteableRegistry namedWriteableRegistry,
+        final CircuitBreakerService circuitBreakerService,
+        final SharedGroupFactory sharedGroupFactory,
+        final SecureTransportSettingsProvider secureTransportSettingsProvider,
+        final Tracer tracer
+    ) {
+        super(
+            settings,
+            version,
+            threadPool,
+            networkService,
+            pageCacheRecycler,
+            namedWriteableRegistry,
+            circuitBreakerService,
+            sharedGroupFactory,
+            tracer
+        );
+
+        this.secureTransportSettingsProvider = secureTransportSettingsProvider;
+        this.exceptionHandler = secureTransportSettingsProvider.buildServerTransportExceptionHandler(settings, this)
+            .orElse(SecureTransportSettingsProvider.ServerExceptionHandler.NOOP);
+    }
+
+    @Override
+    public void onException(TcpChannel channel, Exception e) {
+
+        Throwable cause = e;
+
+        if (e instanceof DecoderException && e != null) {
+            cause = e.getCause();
+        }
+
+        exceptionHandler.onError(cause);
+        logger.error("Exception during establishing a SSL connection: " + cause, cause);
+
+        if (channel == null || !channel.isOpen()) {
+            throw new OpenSearchSecurityException("The provided TCP channel is invalid.", e);
+        }
+        super.onException(channel, e);
+    }
+
+    @Override
+    protected ChannelHandler getServerChannelInitializer(String name) {
+        return new SSLServerChannelInitializer(name);
+    }
+
+    @Override
+    protected ChannelHandler getClientChannelInitializer(DiscoveryNode node) {
+        return new SSLClientChannelInitializer(node);
+    }
+
+    protected class SSLServerChannelInitializer extends Netty4Transport.ServerChannelInitializer {
+
+        public SSLServerChannelInitializer(String name) {
+            super(name);
+        }
+
+        @Override
+        protected void initChannel(Channel ch) throws Exception {
+            super.initChannel(ch);
+
+            final boolean dualModeEnabled = NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED.get(settings);
+            if (dualModeEnabled) {
+                logger.info("SSL Dual mode enabled, using port unification handler");
+                final ChannelHandler portUnificationHandler = new DualModeSslHandler(
+                    settings,
+                    secureTransportSettingsProvider,
+                    SecureNetty4Transport.this
+                );
+                ch.pipeline().addFirst("port_unification_handler", portUnificationHandler);
+            } else {
+                final SSLEngine sslEngine = secureTransportSettingsProvider.buildSecureServerTransportEngine(
+                    settings,
+                    SecureNetty4Transport.this
+                ).orElseGet(SslUtils::createDefaultServerSSLEngine);
+                final SslHandler sslHandler = new SslHandler(sslEngine);
+                ch.pipeline().addFirst("ssl_server", sslHandler);
+            }
+        }
+
+        @Override
+        public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+            if (cause instanceof DecoderException && cause != null) {
+                cause = cause.getCause();
+            }
+
+            logger.error("Exception during establishing a SSL connection: " + cause, cause);
+
+            super.exceptionCaught(ctx, cause);
+        }
+    }
+
+    protected static class ClientSSLHandler extends ChannelOutboundHandlerAdapter {
+        private final Logger log = LogManager.getLogger(this.getClass());
+        private final Settings settings;
+        private final SecureTransportSettingsProvider secureTransportSettingsProvider;
+        private final boolean hostnameVerificationEnabled;
+        private final boolean hostnameVerificationResovleHostName;
+
+        private ClientSSLHandler(
+            final Settings settings,
+            final SecureTransportSettingsProvider secureTransportSettingsProvider,
+            final boolean hostnameVerificationEnabled,
+            final boolean hostnameVerificationResovleHostName
+        ) {
+            this.settings = settings;
+            this.secureTransportSettingsProvider = secureTransportSettingsProvider;
+            this.hostnameVerificationEnabled = hostnameVerificationEnabled;
+            this.hostnameVerificationResovleHostName = hostnameVerificationResovleHostName;
+        }
+
+        @Override
+        public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+            if (cause instanceof DecoderException && cause != null) {
+                cause = cause.getCause();
+            }
+
+            logger.error("Exception during establishing a SSL connection: " + cause, cause);
+
+            super.exceptionCaught(ctx, cause);
+        }
+
+        @SuppressForbidden(reason = "The java.net.InetSocketAddress#getHostName() needs to be used")
+        @Override
+        public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise)
+            throws Exception {
+            SSLEngine sslEngine = null;
+            try {
+                if (hostnameVerificationEnabled) {
+                    final InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteAddress;
+                    final String hostname = (hostnameVerificationResovleHostName == true)
+                        ? inetSocketAddress.getHostName()
+                        : inetSocketAddress.getHostString();
+
+                    if (log.isDebugEnabled()) {
+                        log.debug(
+                            "Hostname of peer is {} ({}/{}) with hostnameVerificationResolveHostName: {}",
+                            hostname,
+                            inetSocketAddress.getHostName(),
+                            inetSocketAddress.getHostString(),
+                            hostnameVerificationResovleHostName
+                        );
+                    }
+
+                    sslEngine = secureTransportSettingsProvider.buildSecureClientTransportEngine(
+                        settings,
+                        hostname,
+                        inetSocketAddress.getPort()
+                    ).orElse(null);
+
+                } else {
+                    sslEngine = secureTransportSettingsProvider.buildSecureClientTransportEngine(settings, null, -1).orElse(null);
+                }
+
+                if (sslEngine == null) {
+                    sslEngine = SslUtils.createDefaultClientSSLEngine();
+                }
+            } catch (final SSLException e) {
+                throw ExceptionsHelper.convertToOpenSearchException(e);
+            }
+
+            final SslHandler sslHandler = new SslHandler(sslEngine);
+            ctx.pipeline().replace(this, "ssl_client", sslHandler);
+            super.connect(ctx, remoteAddress, localAddress, promise);
+        }
+    }
+
+    protected class SSLClientChannelInitializer extends Netty4Transport.ClientChannelInitializer {
+        private final boolean hostnameVerificationEnabled;
+        private final boolean hostnameVerificationResolveHostName;
+        private final DiscoveryNode node;
+        private SSLConnectionTestResult connectionTestResult;
+
+        @SuppressWarnings("removal")
+        public SSLClientChannelInitializer(DiscoveryNode node) {
+            this.node = node;
+
+            final boolean dualModeEnabled = NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED.get(settings);
+            hostnameVerificationEnabled = NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION.get(settings);
+            hostnameVerificationResolveHostName = NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.get(settings);
+
+            connectionTestResult = SSLConnectionTestResult.SSL_AVAILABLE;
+            if (dualModeEnabled) {
+                SecureConnectionTestUtil sslConnectionTestUtil = new SecureConnectionTestUtil(
+                    node.getAddress().getAddress(),
+                    node.getAddress().getPort()
+                );
+                connectionTestResult = AccessController.doPrivileged(
+                    (PrivilegedAction<SSLConnectionTestResult>) sslConnectionTestUtil::testConnection
+                );
+            }
+        }
+
+        @Override
+        protected void initChannel(Channel ch) throws Exception {
+            super.initChannel(ch);
+
+            if (connectionTestResult == SSLConnectionTestResult.OPENSEARCH_PING_FAILED) {
+                logger.error(
+                    "SSL dual mode is enabled but dual mode handshake and OpenSearch ping has failed during client connection setup, closing channel"
+                );
+                ch.close();
+                return;
+            }
+
+            if (connectionTestResult == SSLConnectionTestResult.SSL_AVAILABLE) {
+                logger.debug("Connection to {} needs to be ssl, adding ssl handler to the client channel ", node.getHostName());
+                ch.pipeline()
+                    .addFirst(
+                        "client_ssl_handler",
+                        new ClientSSLHandler(
+                            settings,
+                            secureTransportSettingsProvider,
+                            hostnameVerificationEnabled,
+                            hostnameVerificationResolveHostName
+                        )
+                    );
+            } else {
+                logger.debug("Connection to {} needs to be non ssl", node.getHostName());
+            }
+        }
+
+        @Override
+        public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+            if (cause instanceof DecoderException && cause != null) {
+                cause = cause.getCause();
+            }
+
+            logger.error("Exception during establishing a SSL connection: " + cause, cause);
+
+            super.exceptionCaught(ctx, cause);
+        }
+    }
+}
diff --git a/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SslUtils.java b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SslUtils.java
new file mode 100644
index 0000000000000..8b8223da70c08
--- /dev/null
+++ b/modules/transport-netty4/src/main/java/org/opensearch/transport/netty4/ssl/SslUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ *
+ * Modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+package org.opensearch.transport.netty4.ssl;
+
+import org.opensearch.OpenSearchSecurityException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+import java.nio.ByteOrder;
+import java.security.NoSuchAlgorithmException;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * @see <a href="https://github.com/opensearch-project/security/blob/d526c9f6c2a438c14db8b413148204510b9fe2e2/src/main/java/org/opensearch/security/ssl/util/TLSUtil.java">TLSUtil</a>
+ */
+public class SslUtils {
+    private static final String[] DEFAULT_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" };
+
+    private static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20;
+    private static final int SSL_CONTENT_TYPE_ALERT = 21;
+    private static final int SSL_CONTENT_TYPE_HANDSHAKE = 22;
+    private static final int SSL_CONTENT_TYPE_APPLICATION_DATA = 23;
+    // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions heartbeat needs special handling by security extension
+    private static final int SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT = 24;
+    // CS-ENFORCE-SINGLE
+    private static final int SSL_RECORD_HEADER_LENGTH = 5;
+
+    private SslUtils() {
+
+    }
+
+    public static SSLEngine createDefaultServerSSLEngine() {
+        try {
+            final SSLEngine engine = SSLContext.getDefault().createSSLEngine();
+            engine.setEnabledProtocols(DEFAULT_SSL_PROTOCOLS);
+            engine.setUseClientMode(false);
+            return engine;
+        } catch (final NoSuchAlgorithmException ex) {
+            throw new OpenSearchSecurityException("Unable to initialize default server SSL engine", ex);
+        }
+    }
+
+    public static SSLEngine createDefaultClientSSLEngine() {
+        try {
+            final SSLEngine engine = SSLContext.getDefault().createSSLEngine();
+            engine.setEnabledProtocols(DEFAULT_SSL_PROTOCOLS);
+            engine.setUseClientMode(true);
+            return engine;
+        } catch (final NoSuchAlgorithmException ex) {
+            throw new OpenSearchSecurityException("Unable to initialize default client SSL engine", ex);
+        }
+    }
+
+    static boolean isTLS(ByteBuf buffer) {
+        int packetLength = 0;
+        int offset = buffer.readerIndex();
+
+        // SSLv3 or TLS - Check ContentType
+        boolean tls;
+        switch (buffer.getUnsignedByte(offset)) {
+            case SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
+            case SSL_CONTENT_TYPE_ALERT:
+            case SSL_CONTENT_TYPE_HANDSHAKE:
+            case SSL_CONTENT_TYPE_APPLICATION_DATA:
+                // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions heartbeat needs special handling by security extension
+            case SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT:
+                tls = true;
+                break;
+            // CS-ENFORCE-SINGLE
+            default:
+                // SSLv2 or bad data
+                tls = false;
+        }
+
+        if (tls) {
+            // SSLv3 or TLS - Check ProtocolVersion
+            int majorVersion = buffer.getUnsignedByte(offset + 1);
+            if (majorVersion == 3) {
+                // SSLv3 or TLS
+                packetLength = unsignedShortBE(buffer, offset + 3) + SSL_RECORD_HEADER_LENGTH;
+                if (packetLength <= SSL_RECORD_HEADER_LENGTH) {
+                    // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data)
+                    tls = false;
+                }
+            } else {
+                // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data)
+                tls = false;
+            }
+        }
+
+        return tls;
+    }
+
+    private static int unsignedShortBE(ByteBuf buffer, int offset) {
+        return buffer.order() == ByteOrder.BIG_ENDIAN ? buffer.getUnsignedShort(offset) : buffer.getUnsignedShortLE(offset);
+    }
+}
diff --git a/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/Netty4HttpClient.java b/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/Netty4HttpClient.java
index 1a9f2a558ecd8..f33a1b8ba989f 100644
--- a/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/Netty4HttpClient.java
+++ b/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/Netty4HttpClient.java
@@ -37,6 +37,7 @@
 import org.opensearch.core.common.unit.ByteSizeValue;
 import org.opensearch.tasks.Task;
 import org.opensearch.transport.NettyAllocator;
+import org.opensearch.transport.netty4.ssl.TrustAllManager;
 
 import java.io.Closeable;
 import java.net.SocketAddress;
@@ -71,6 +72,9 @@
 import io.netty.handler.codec.http.HttpResponse;
 import io.netty.handler.codec.http.HttpResponseDecoder;
 import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
 
 import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
@@ -79,7 +83,7 @@
 /**
  * Tiny helper to send http requests over netty.
  */
-class Netty4HttpClient implements Closeable {
+public class Netty4HttpClient implements Closeable {
 
     static Collection<String> returnHttpResponseBodies(Collection<FullHttpResponse> responses) {
         List<String> list = new ArrayList<>(responses.size());
@@ -98,11 +102,21 @@ static Collection<String> returnOpaqueIds(Collection<FullHttpResponse> responses
     }
 
     private final Bootstrap clientBootstrap;
+    private final boolean secure;
 
     Netty4HttpClient() {
+        this(false);
+    }
+
+    private Netty4HttpClient(boolean secure) {
         clientBootstrap = new Bootstrap().channel(NettyAllocator.getChannelType())
             .option(ChannelOption.ALLOCATOR, NettyAllocator.getAllocator())
             .group(new NioEventLoopGroup(1));
+        this.secure = secure;
+    }
+
+    public static Netty4HttpClient https() {
+        return new Netty4HttpClient(true);
     }
 
     public List<FullHttpResponse> get(SocketAddress remoteAddress, String... uris) throws InterruptedException {
@@ -154,7 +168,7 @@ private synchronized List<FullHttpResponse> sendRequests(final SocketAddress rem
         final CountDownLatch latch = new CountDownLatch(requests.size());
         final List<FullHttpResponse> content = Collections.synchronizedList(new ArrayList<>(requests.size()));
 
-        clientBootstrap.handler(new CountDownLatchHandler(latch, content));
+        clientBootstrap.handler(new CountDownLatchHandler(latch, content, secure));
 
         ChannelFuture channelFuture = null;
         try {
@@ -189,19 +203,32 @@ private static class CountDownLatchHandler extends ChannelInitializer<SocketChan
 
         private final CountDownLatch latch;
         private final Collection<FullHttpResponse> content;
+        private final boolean secure;
 
-        CountDownLatchHandler(final CountDownLatch latch, final Collection<FullHttpResponse> content) {
+        CountDownLatchHandler(final CountDownLatch latch, final Collection<FullHttpResponse> content, final boolean secure) {
             this.latch = latch;
             this.content = content;
+            this.secure = secure;
         }
 
         @Override
-        protected void initChannel(SocketChannel ch) {
+        protected void initChannel(SocketChannel ch) throws Exception {
             final int maxContentLength = new ByteSizeValue(100, ByteSizeUnit.MB).bytesAsInt();
             ch.pipeline().addLast(new HttpResponseDecoder());
             ch.pipeline().addLast(new HttpRequestEncoder());
             ch.pipeline().addLast(new HttpContentDecompressor());
             ch.pipeline().addLast(new HttpObjectAggregator(maxContentLength));
+            if (secure) {
+                final SslHandler sslHandler = new SslHandler(
+                    SslContextBuilder.forClient()
+                        .clientAuth(ClientAuth.NONE)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(ch.alloc())
+                );
+                ch.pipeline().addFirst("client_ssl_handler", sslHandler);
+            }
+
             ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpObject>() {
                 @Override
                 protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
diff --git a/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransportTests.java b/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransportTests.java
new file mode 100644
index 0000000000000..9ea49d0b24d44
--- /dev/null
+++ b/modules/transport-netty4/src/test/java/org/opensearch/http/netty4/ssl/SecureNetty4HttpServerTransportTests.java
@@ -0,0 +1,603 @@
+/*
+ * 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.http.netty4.ssl;
+
+import org.apache.logging.log4j.message.ParameterizedMessage;
+import org.opensearch.OpenSearchException;
+import org.opensearch.common.network.NetworkAddress;
+import org.opensearch.common.network.NetworkService;
+import org.opensearch.common.settings.ClusterSettings;
+import org.opensearch.common.settings.Setting;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.common.unit.TimeValue;
+import org.opensearch.common.util.MockBigArrays;
+import org.opensearch.common.util.MockPageCacheRecycler;
+import org.opensearch.common.util.concurrent.ThreadContext;
+import org.opensearch.core.common.bytes.BytesArray;
+import org.opensearch.core.common.transport.TransportAddress;
+import org.opensearch.core.common.unit.ByteSizeValue;
+import org.opensearch.core.indices.breaker.NoneCircuitBreakerService;
+import org.opensearch.http.BindHttpException;
+import org.opensearch.http.CorsHandler;
+import org.opensearch.http.HttpServerTransport;
+import org.opensearch.http.HttpTransportSettings;
+import org.opensearch.http.NullDispatcher;
+import org.opensearch.http.netty4.Netty4HttpClient;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
+import org.opensearch.rest.BytesRestResponse;
+import org.opensearch.rest.RestChannel;
+import org.opensearch.rest.RestRequest;
+import org.opensearch.telemetry.tracing.noop.NoopTracer;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.test.rest.FakeRestRequest;
+import org.opensearch.threadpool.TestThreadPool;
+import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.NettyAllocator;
+import org.opensearch.transport.SharedGroupFactory;
+import org.opensearch.transport.TcpTransport;
+import org.opensearch.transport.netty4.ssl.TrustAllManager;
+import org.junit.After;
+import org.junit.Before;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.TooLongFrameException;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContextBuilder;
+
+import static org.opensearch.core.rest.RestStatus.BAD_REQUEST;
+import static org.opensearch.core.rest.RestStatus.OK;
+import static org.opensearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
+import static org.opensearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for the {@link SecureNetty4HttpServerTransport} class.
+ */
+public class SecureNetty4HttpServerTransportTests extends OpenSearchTestCase {
+
+    private NetworkService networkService;
+    private ThreadPool threadPool;
+    private MockBigArrays bigArrays;
+    private ClusterSettings clusterSettings;
+    private SecureTransportSettingsProvider secureTransportSettingsProvider;
+
+    @Before
+    public void setup() throws Exception {
+        networkService = new NetworkService(Collections.emptyList());
+        threadPool = new TestThreadPool("test");
+        bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService());
+        clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
+
+        secureTransportSettingsProvider = new SecureTransportSettingsProvider() {
+            @Override
+            public Optional<ServerExceptionHandler> buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<ServerExceptionHandler> buildServerTransportExceptionHandler(Settings settings, TcpTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException {
+                try {
+                    final KeyStore keyStore = KeyStore.getInstance("PKCS12");
+                    keyStore.load(
+                        SecureNetty4HttpServerTransportTests.class.getResourceAsStream("/netty4-secure.jks"),
+                        "password".toCharArray()
+                    );
+
+                    final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
+                    keyManagerFactory.init(keyStore, "password".toCharArray());
+
+                    SSLEngine engine = SslContextBuilder.forServer(keyManagerFactory)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(NettyAllocator.getAllocator());
+                    return Optional.of(engine);
+                } catch (final IOException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException
+                    | CertificateException ex) {
+                    throw new SSLException(ex);
+                }
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureServerTransportEngine(Settings settings, TcpTransport transport) throws SSLException {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException {
+                return Optional.of(
+                    SslContextBuilder.forClient()
+                        .clientAuth(ClientAuth.NONE)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(NettyAllocator.getAllocator())
+                );
+            }
+        };
+    }
+
+    @After
+    public void shutdown() throws Exception {
+        if (threadPool != null) {
+            threadPool.shutdownNow();
+        }
+        threadPool = null;
+        networkService = null;
+        bigArrays = null;
+        clusterSettings = null;
+    }
+
+    /**
+     * Test that {@link SecureNetty4HttpServerTransport} supports the "Expect: 100-continue" HTTP header
+     * @throws InterruptedException if the client communication with the server is interrupted
+     */
+    public void testExpectContinueHeader() throws InterruptedException {
+        final Settings settings = createSettings();
+        final int contentLength = randomIntBetween(1, HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH.get(settings).bytesAsInt());
+        runExpectHeaderTest(settings, HttpHeaderValues.CONTINUE.toString(), contentLength, HttpResponseStatus.CONTINUE);
+    }
+
+    /**
+     * Test that {@link SecureNetty4HttpServerTransport} responds to a
+     * 100-continue expectation with too large a content-length
+     * with a 413 status.
+     * @throws InterruptedException if the client communication with the server is interrupted
+     */
+    public void testExpectContinueHeaderContentLengthTooLong() throws InterruptedException {
+        final String key = HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH.getKey();
+        final int maxContentLength = randomIntBetween(1, 104857600);
+        final Settings settings = createBuilderWithPort().put(key, maxContentLength + "b").build();
+        final int contentLength = randomIntBetween(maxContentLength + 1, Integer.MAX_VALUE);
+        runExpectHeaderTest(settings, HttpHeaderValues.CONTINUE.toString(), contentLength, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+    }
+
+    /**
+     * Test that {@link SecureNetty4HttpServerTransport} responds to an unsupported expectation with a 417 status.
+     * @throws InterruptedException if the client communication with the server is interrupted
+     */
+    public void testExpectUnsupportedExpectation() throws InterruptedException {
+        Settings settings = createSettings();
+        runExpectHeaderTest(settings, "chocolate=yummy", 0, HttpResponseStatus.EXPECTATION_FAILED);
+    }
+
+    private void runExpectHeaderTest(
+        final Settings settings,
+        final String expectation,
+        final int contentLength,
+        final HttpResponseStatus expectedStatus
+    ) throws InterruptedException {
+
+        final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
+            @Override
+            public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) {
+                channel.sendResponse(new BytesRestResponse(OK, BytesRestResponse.TEXT_CONTENT_TYPE, new BytesArray("done")));
+            }
+
+            @Override
+            public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, Throwable cause) {
+                logger.error(
+                    new ParameterizedMessage("--> Unexpected bad request [{}]", FakeRestRequest.requestToString(channel.request())),
+                    cause
+                );
+                throw new AssertionError();
+            }
+        };
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                settings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                dispatcher,
+                clusterSettings,
+                new SharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+            try (Netty4HttpClient client = Netty4HttpClient.https()) {
+                final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
+                request.headers().set(HttpHeaderNames.EXPECT, expectation);
+                HttpUtil.setContentLength(request, contentLength);
+
+                final FullHttpResponse response = client.send(remoteAddress.address(), request);
+                try {
+                    assertThat(response.status(), equalTo(expectedStatus));
+                    if (expectedStatus.equals(HttpResponseStatus.CONTINUE)) {
+                        final FullHttpRequest continuationRequest = new DefaultFullHttpRequest(
+                            HttpVersion.HTTP_1_1,
+                            HttpMethod.POST,
+                            "/",
+                            Unpooled.EMPTY_BUFFER
+                        );
+                        final FullHttpResponse continuationResponse = client.send(remoteAddress.address(), continuationRequest);
+                        try {
+                            assertThat(continuationResponse.status(), is(HttpResponseStatus.OK));
+                            assertThat(
+                                new String(ByteBufUtil.getBytes(continuationResponse.content()), StandardCharsets.UTF_8),
+                                is("done")
+                            );
+                        } finally {
+                            continuationResponse.release();
+                        }
+                    }
+                } finally {
+                    response.release();
+                }
+            }
+        }
+    }
+
+    public void testBindUnavailableAddress() {
+        Settings initialSettings = createSettings();
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                initialSettings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                new NullDispatcher(),
+                clusterSettings,
+                new SharedGroupFactory(Settings.EMPTY),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+            Settings settings = Settings.builder()
+                .put("http.port", remoteAddress.getPort())
+                .put("network.host", remoteAddress.getAddress())
+                .build();
+            try (
+                SecureNetty4HttpServerTransport otherTransport = new SecureNetty4HttpServerTransport(
+                    settings,
+                    networkService,
+                    bigArrays,
+                    threadPool,
+                    xContentRegistry(),
+                    new NullDispatcher(),
+                    clusterSettings,
+                    new SharedGroupFactory(settings),
+                    secureTransportSettingsProvider,
+                    NoopTracer.INSTANCE
+                )
+            ) {
+                BindHttpException bindHttpException = expectThrows(BindHttpException.class, otherTransport::start);
+                assertEquals("Failed to bind to " + NetworkAddress.format(remoteAddress.address()), bindHttpException.getMessage());
+            }
+        }
+    }
+
+    public void testBadRequest() throws InterruptedException {
+        final AtomicReference<Throwable> causeReference = new AtomicReference<>();
+        final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
+
+            @Override
+            public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) {
+                logger.error("--> Unexpected successful request [{}]", FakeRestRequest.requestToString(request));
+                throw new AssertionError();
+            }
+
+            @Override
+            public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
+                causeReference.set(cause);
+                try {
+                    final OpenSearchException e = new OpenSearchException("you sent a bad request and you should feel bad");
+                    channel.sendResponse(new BytesRestResponse(channel, BAD_REQUEST, e));
+                } catch (final IOException e) {
+                    throw new AssertionError(e);
+                }
+            }
+
+        };
+
+        final Settings settings;
+        final int maxInitialLineLength;
+        final Setting<ByteSizeValue> httpMaxInitialLineLengthSetting = HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH;
+        if (randomBoolean()) {
+            maxInitialLineLength = httpMaxInitialLineLengthSetting.getDefault(Settings.EMPTY).bytesAsInt();
+            settings = createSettings();
+        } else {
+            maxInitialLineLength = randomIntBetween(1, 8192);
+            settings = createBuilderWithPort().put(httpMaxInitialLineLengthSetting.getKey(), maxInitialLineLength + "b").build();
+        }
+
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                settings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                dispatcher,
+                clusterSettings,
+                new SharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+
+            try (Netty4HttpClient client = Netty4HttpClient.https()) {
+                final String url = "/" + new String(new byte[maxInitialLineLength], Charset.forName("UTF-8"));
+                final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url);
+
+                final FullHttpResponse response = client.send(remoteAddress.address(), request);
+                try {
+                    assertThat(response.status(), equalTo(HttpResponseStatus.BAD_REQUEST));
+                    assertThat(
+                        new String(response.content().array(), Charset.forName("UTF-8")),
+                        containsString("you sent a bad request and you should feel bad")
+                    );
+                } finally {
+                    response.release();
+                }
+            }
+        }
+
+        assertNotNull(causeReference.get());
+        assertThat(causeReference.get(), instanceOf(TooLongFrameException.class));
+    }
+
+    public void testLargeCompressedResponse() throws InterruptedException {
+        final String responseString = randomAlphaOfLength(4 * 1024 * 1024);
+        final String url = "/thing";
+        final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
+
+            @Override
+            public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) {
+                if (url.equals(request.uri())) {
+                    channel.sendResponse(new BytesRestResponse(OK, responseString));
+                } else {
+                    logger.error("--> Unexpected successful uri [{}]", request.uri());
+                    throw new AssertionError();
+                }
+            }
+
+            @Override
+            public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
+                logger.error(
+                    new ParameterizedMessage("--> Unexpected bad request [{}]", FakeRestRequest.requestToString(channel.request())),
+                    cause
+                );
+                throw new AssertionError();
+            }
+
+        };
+
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                Settings.EMPTY,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                dispatcher,
+                clusterSettings,
+                new SharedGroupFactory(Settings.EMPTY),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+
+            try (Netty4HttpClient client = Netty4HttpClient.https()) {
+                DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url);
+                request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, randomFrom("deflate", "gzip"));
+                final FullHttpResponse response = client.send(remoteAddress.address(), request);
+                try {
+                    assertThat(response.status(), equalTo(HttpResponseStatus.OK));
+                    byte[] bytes = new byte[response.content().readableBytes()];
+                    response.content().readBytes(bytes);
+                    assertThat(new String(bytes, StandardCharsets.UTF_8), equalTo(responseString));
+                } finally {
+                    response.release();
+                }
+            }
+        }
+    }
+
+    public void testCorsRequest() throws InterruptedException {
+        final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
+
+            @Override
+            public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) {
+                logger.error("--> Unexpected successful request [{}]", FakeRestRequest.requestToString(request));
+                throw new AssertionError();
+            }
+
+            @Override
+            public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
+                logger.error(
+                    new ParameterizedMessage("--> Unexpected bad request [{}]", FakeRestRequest.requestToString(channel.request())),
+                    cause
+                );
+                throw new AssertionError();
+            }
+
+        };
+
+        final Settings settings = createBuilderWithPort().put(SETTING_CORS_ENABLED.getKey(), true)
+            .put(SETTING_CORS_ALLOW_ORIGIN.getKey(), "test-cors.org")
+            .build();
+
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                settings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                dispatcher,
+                new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS),
+                new SharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+
+            // Test pre-flight request
+            try (Netty4HttpClient client = Netty4HttpClient.https()) {
+                final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/");
+                request.headers().add(CorsHandler.ORIGIN, "test-cors.org");
+                request.headers().add(CorsHandler.ACCESS_CONTROL_REQUEST_METHOD, "POST");
+
+                final FullHttpResponse response = client.send(remoteAddress.address(), request);
+                try {
+                    assertThat(response.status(), equalTo(HttpResponseStatus.OK));
+                    assertThat(response.headers().get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("test-cors.org"));
+                    assertThat(response.headers().get(CorsHandler.VARY), equalTo(CorsHandler.ORIGIN));
+                    assertTrue(response.headers().contains(CorsHandler.DATE));
+                } finally {
+                    response.release();
+                }
+            }
+
+            // Test short-circuited request
+            try (Netty4HttpClient client = Netty4HttpClient.https()) {
+                final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
+                request.headers().add(CorsHandler.ORIGIN, "google.com");
+
+                final FullHttpResponse response = client.send(remoteAddress.address(), request);
+                try {
+                    assertThat(response.status(), equalTo(HttpResponseStatus.FORBIDDEN));
+                } finally {
+                    response.release();
+                }
+            }
+        }
+    }
+
+    public void testReadTimeout() throws Exception {
+        final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
+
+            @Override
+            public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) {
+                logger.error("--> Unexpected successful request [{}]", FakeRestRequest.requestToString(request));
+                throw new AssertionError("Should not have received a dispatched request");
+            }
+
+            @Override
+            public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
+                logger.error(
+                    new ParameterizedMessage("--> Unexpected bad request [{}]", FakeRestRequest.requestToString(channel.request())),
+                    cause
+                );
+                throw new AssertionError("Should not have received a dispatched request");
+            }
+
+        };
+
+        Settings settings = createBuilderWithPort().put(
+            HttpTransportSettings.SETTING_HTTP_READ_TIMEOUT.getKey(),
+            new TimeValue(randomIntBetween(100, 300))
+        ).build();
+
+        NioEventLoopGroup group = new NioEventLoopGroup();
+        try (
+            SecureNetty4HttpServerTransport transport = new SecureNetty4HttpServerTransport(
+                settings,
+                networkService,
+                bigArrays,
+                threadPool,
+                xContentRegistry(),
+                dispatcher,
+                new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS),
+                new SharedGroupFactory(settings),
+                secureTransportSettingsProvider,
+                NoopTracer.INSTANCE
+            )
+        ) {
+            transport.start();
+            final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
+
+            CountDownLatch channelClosedLatch = new CountDownLatch(1);
+
+            Bootstrap clientBootstrap = new Bootstrap().option(ChannelOption.ALLOCATOR, NettyAllocator.getAllocator())
+                .channel(NioSocketChannel.class)
+                .handler(new ChannelInitializer<SocketChannel>() {
+
+                    @Override
+                    protected void initChannel(SocketChannel ch) {
+                        ch.pipeline().addLast(new ChannelHandlerAdapter() {
+                        });
+
+                    }
+                })
+                .group(group);
+            ChannelFuture connect = clientBootstrap.connect(remoteAddress.address());
+            connect.channel().closeFuture().addListener(future -> channelClosedLatch.countDown());
+
+            assertTrue("Channel should be closed due to read timeout", channelClosedLatch.await(1, TimeUnit.MINUTES));
+
+        } finally {
+            group.shutdownGracefully().await();
+        }
+    }
+
+    private Settings createSettings() {
+        return createBuilderWithPort().build();
+    }
+
+    private Settings.Builder createBuilderWithPort() {
+        return Settings.builder().put(HttpTransportSettings.SETTING_HTTP_PORT.getKey(), getPortRange());
+    }
+}
diff --git a/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/SimpleSecureNetty4TransportTests.java b/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/SimpleSecureNetty4TransportTests.java
new file mode 100644
index 0000000000000..0cae58b8efa2a
--- /dev/null
+++ b/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/SimpleSecureNetty4TransportTests.java
@@ -0,0 +1,234 @@
+/*
+ * 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.netty4.ssl;
+
+import org.opensearch.Version;
+import org.opensearch.cluster.node.DiscoveryNode;
+import org.opensearch.common.network.NetworkService;
+import org.opensearch.common.settings.ClusterSettings;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.common.util.PageCacheRecycler;
+import org.opensearch.common.util.io.IOUtils;
+import org.opensearch.common.util.net.NetUtils;
+import org.opensearch.core.action.ActionListener;
+import org.opensearch.core.common.io.stream.NamedWriteableRegistry;
+import org.opensearch.core.common.transport.TransportAddress;
+import org.opensearch.core.indices.breaker.NoneCircuitBreakerService;
+import org.opensearch.http.HttpServerTransport;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
+import org.opensearch.telemetry.tracing.noop.NoopTracer;
+import org.opensearch.test.transport.MockTransportService;
+import org.opensearch.test.transport.StubbableTransport;
+import org.opensearch.transport.AbstractSimpleTransportTestCase;
+import org.opensearch.transport.ConnectTransportException;
+import org.opensearch.transport.ConnectionProfile;
+import org.opensearch.transport.Netty4NioSocketChannel;
+import org.opensearch.transport.NettyAllocator;
+import org.opensearch.transport.SharedGroupFactory;
+import org.opensearch.transport.TcpChannel;
+import org.opensearch.transport.TcpTransport;
+import org.opensearch.transport.TestProfiles;
+import org.opensearch.transport.Transport;
+import org.opensearch.transport.netty4.Netty4TcpChannel;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.channels.SocketChannel;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Optional;
+
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContextBuilder;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+
+public class SimpleSecureNetty4TransportTests extends AbstractSimpleTransportTestCase {
+    @Override
+    protected Transport build(Settings settings, final Version version, ClusterSettings clusterSettings, boolean doHandshake) {
+        NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(Collections.emptyList());
+        final SecureTransportSettingsProvider secureTransportSettingsProvider = new SecureTransportSettingsProvider() {
+            @Override
+            public Optional<ServerExceptionHandler> buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<ServerExceptionHandler> buildServerTransportExceptionHandler(Settings settings, TcpTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException {
+                try {
+                    final KeyStore keyStore = KeyStore.getInstance("PKCS12");
+                    keyStore.load(
+                        SimpleSecureNetty4TransportTests.class.getResourceAsStream("/netty4-secure.jks"),
+                        "password".toCharArray()
+                    );
+
+                    final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
+                    keyManagerFactory.init(keyStore, "password".toCharArray());
+
+                    SSLEngine engine = SslContextBuilder.forServer(keyManagerFactory)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(NettyAllocator.getAllocator());
+                    return Optional.of(engine);
+                } catch (final IOException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException
+                    | CertificateException ex) {
+                    throw new SSLException(ex);
+                }
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureServerTransportEngine(Settings settings, TcpTransport transport) throws SSLException {
+                try {
+                    final KeyStore keyStore = KeyStore.getInstance("PKCS12");
+                    keyStore.load(
+                        SimpleSecureNetty4TransportTests.class.getResourceAsStream("/netty4-secure.jks"),
+                        "password".toCharArray()
+                    );
+
+                    final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
+                    keyManagerFactory.init(keyStore, "password".toCharArray());
+
+                    SSLEngine engine = SslContextBuilder.forServer(keyManagerFactory)
+                        .clientAuth(ClientAuth.NONE)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(NettyAllocator.getAllocator());
+                    return Optional.of(engine);
+                } catch (final IOException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException
+                    | CertificateException ex) {
+                    throw new SSLException(ex);
+                }
+
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException {
+                return Optional.of(
+                    SslContextBuilder.forClient()
+                        .clientAuth(ClientAuth.NONE)
+                        .trustManager(TrustAllManager.INSTANCE)
+                        .build()
+                        .newEngine(NettyAllocator.getAllocator())
+                );
+            }
+        };
+
+        return new SecureNetty4Transport(
+            settings,
+            version,
+            threadPool,
+            new NetworkService(Collections.emptyList()),
+            PageCacheRecycler.NON_RECYCLING_INSTANCE,
+            namedWriteableRegistry,
+            new NoneCircuitBreakerService(),
+            new SharedGroupFactory(settings),
+            secureTransportSettingsProvider,
+            NoopTracer.INSTANCE
+        ) {
+
+            @Override
+            public void executeHandshake(
+                DiscoveryNode node,
+                TcpChannel channel,
+                ConnectionProfile profile,
+                ActionListener<Version> listener
+            ) {
+                if (doHandshake) {
+                    super.executeHandshake(node, channel, profile, listener);
+                } else {
+                    listener.onResponse(version.minimumCompatibilityVersion());
+                }
+            }
+        };
+    }
+
+    public void testConnectException() throws UnknownHostException {
+        try {
+            serviceA.connectToNode(
+                new DiscoveryNode(
+                    "C",
+                    new TransportAddress(InetAddress.getByName("localhost"), 9876),
+                    emptyMap(),
+                    emptySet(),
+                    Version.CURRENT
+                )
+            );
+            fail("Expected ConnectTransportException");
+        } catch (ConnectTransportException e) {
+            assertThat(e.getMessage(), containsString("connect_exception"));
+            assertThat(e.getMessage(), containsString("[127.0.0.1:9876]"));
+        }
+    }
+
+    public void testDefaultKeepAliveSettings() throws IOException {
+        assumeTrue("setting default keepalive options not supported on this platform", (IOUtils.LINUX || IOUtils.MAC_OS_X));
+        try (
+            MockTransportService serviceC = buildService("TS_C", Version.CURRENT, Settings.EMPTY);
+            MockTransportService serviceD = buildService("TS_D", Version.CURRENT, Settings.EMPTY)
+        ) {
+            serviceC.start();
+            serviceC.acceptIncomingRequests();
+            serviceD.start();
+            serviceD.acceptIncomingRequests();
+
+            try (Transport.Connection connection = serviceC.openConnection(serviceD.getLocalDiscoNode(), TestProfiles.LIGHT_PROFILE)) {
+                assertThat(connection, instanceOf(StubbableTransport.WrappedConnection.class));
+                Transport.Connection conn = ((StubbableTransport.WrappedConnection) connection).getConnection();
+                assertThat(conn, instanceOf(TcpTransport.NodeChannels.class));
+                TcpTransport.NodeChannels nodeChannels = (TcpTransport.NodeChannels) conn;
+                for (TcpChannel channel : nodeChannels.getChannels()) {
+                    assertFalse(channel.isServerChannel());
+                    checkDefaultKeepAliveOptions(channel);
+                }
+
+                assertThat(serviceD.getOriginalTransport(), instanceOf(TcpTransport.class));
+                for (TcpChannel channel : getAcceptedChannels((TcpTransport) serviceD.getOriginalTransport())) {
+                    assertTrue(channel.isServerChannel());
+                    checkDefaultKeepAliveOptions(channel);
+                }
+            }
+        }
+    }
+
+    private void checkDefaultKeepAliveOptions(TcpChannel channel) throws IOException {
+        assertThat(channel, instanceOf(Netty4TcpChannel.class));
+        Netty4TcpChannel nettyChannel = (Netty4TcpChannel) channel;
+        assertThat(nettyChannel.getNettyChannel(), instanceOf(Netty4NioSocketChannel.class));
+        Netty4NioSocketChannel netty4NioSocketChannel = (Netty4NioSocketChannel) nettyChannel.getNettyChannel();
+        SocketChannel socketChannel = netty4NioSocketChannel.javaChannel();
+        assertThat(socketChannel.supportedOptions(), hasItem(NetUtils.getTcpKeepIdleSocketOptionOrNull()));
+        Integer keepIdle = socketChannel.getOption(NetUtils.getTcpKeepIdleSocketOptionOrNull());
+        assertNotNull(keepIdle);
+        assertThat(keepIdle, lessThanOrEqualTo(500));
+        assertThat(socketChannel.supportedOptions(), hasItem(NetUtils.getTcpKeepIntervalSocketOptionOrNull()));
+        Integer keepInterval = socketChannel.getOption(NetUtils.getTcpKeepIntervalSocketOptionOrNull());
+        assertNotNull(keepInterval);
+        assertThat(keepInterval, lessThanOrEqualTo(500));
+    }
+
+}
diff --git a/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/TrustAllManager.java b/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/TrustAllManager.java
new file mode 100644
index 0000000000000..a38c542b5780e
--- /dev/null
+++ b/modules/transport-netty4/src/test/java/org/opensearch/transport/netty4/ssl/TrustAllManager.java
@@ -0,0 +1,28 @@
+/*
+ * 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.netty4.ssl;
+
+import javax.net.ssl.X509TrustManager;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+public class TrustAllManager implements X509TrustManager {
+    public static final X509TrustManager INSTANCE = new TrustAllManager();
+
+    private TrustAllManager() {}
+
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}
diff --git a/modules/transport-netty4/src/test/resources/README.txt b/modules/transport-netty4/src/test/resources/README.txt
new file mode 100644
index 0000000000000..c8cec5d3803a4
--- /dev/null
+++ b/modules/transport-netty4/src/test/resources/README.txt
@@ -0,0 +1,17 @@
+#!/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 certificate key
+
+openssl req  -x509  -sha256  -newkey rsa:2048  -keyout certificate.key  -out certificate.crt  -days 1024  -nodes
+
+# 2. Export the certificate in pkcs12 format
+
+openssl pkcs12  -export  -in certificate.crt  -inkey certificate.key  -out server.p12  -name netty4-secure -password pass:password
+
+# 3. Import the certificate into JDK keystore (PKCS12 type)
+
+keytool -importkeystore -srcstorepass password  -destkeystore netty4-secure.jks -srckeystore server.p12  -srcstoretype PKCS12  -alias netty4-secure  -deststorepass password
\ No newline at end of file
diff --git a/modules/transport-netty4/src/test/resources/certificate.crt b/modules/transport-netty4/src/test/resources/certificate.crt
new file mode 100644
index 0000000000000..54c78fdbcf6de
--- /dev/null
+++ b/modules/transport-netty4/src/test/resources/certificate.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIUddAawr5zygcd+Dcn9WVDpO4BJ7YwDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
+DTI0MDMxNDE5NDQzOVoXDTI3MDEwMjE5NDQzOVowWTELMAkGA1UEBhMCQVUxEzAR
+BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
+IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAzjOKkg6Iba5zfZ8b/RYw+PGmGEfbdGuuF10Wz4Jmx/Nk4VfDLxdh
+TW8VllUL2JD7uPkjABj7pW3awAbvIJ+VGbKqfBr1Nsz0mPPzhT8cfuMH/FDZgQs3
+4HuqDKr0LfC1Kw5E3WF0GVMBDNu0U+nKoeqySeYjGdxDnd3W4cqK5AnUxL0RnIny
+Bw7ZuhcU55XndH/Xauro/2EpvJduDsWMdqt7ZfIf1TOmaiQHK+82yb/drVaJbczK
+uTpn1Kv2bnzkQEckgq+z1dLNOOyvP2xf+nsziw5ilJe92e5GJOUJYFAlEgUAGpfD
+dv6j/gTRYvdJCJItOQEQtektNCAZsoc0wwIDAQABo1MwUTAdBgNVHQ4EFgQUzHts
+wIt+zhB/R4U4Do2P6rr0YhkwHwYDVR0jBBgwFoAUzHtswIt+zhB/R4U4Do2P6rr0
+YhkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAveh870jJX7vt
+oLCrdugsyo79pR4f7Nr1kUy3jJrfoaoUmrjiiiHWgT22fGwp7j1GZF2mVfo8YVaK
+63YNn5gB2NNZhguPOFC4AdvHRYOKRBOaOvWK8oq7BcJ//18JYI/pPnpgkYvJjqv4
+gFKaZX9qWtujHpAmKiVGs7pwYGNXfixPHRNV4owcfHMIH5dhbbqT49j94xVpjbXs
+OymKtFl4kpCE/0LzKFrFcuu55Am1VLBHx2cPpHLOipgUcF5BHFlQ8AXiCMOwfPAw
+d22mLB6Gt1oVEpyvQHYd3e04FetEXQ9E8T+NKWZx/8Ucf+IWBYmZBRxch6O83xgk
+bAbGzqkbzQ==
+-----END CERTIFICATE-----
diff --git a/modules/transport-netty4/src/test/resources/certificate.key b/modules/transport-netty4/src/test/resources/certificate.key
new file mode 100644
index 0000000000000..228350180935d
--- /dev/null
+++ b/modules/transport-netty4/src/test/resources/certificate.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOM4qSDohtrnN9
+nxv9FjD48aYYR9t0a64XXRbPgmbH82ThV8MvF2FNbxWWVQvYkPu4+SMAGPulbdrA
+Bu8gn5UZsqp8GvU2zPSY8/OFPxx+4wf8UNmBCzfge6oMqvQt8LUrDkTdYXQZUwEM
+27RT6cqh6rJJ5iMZ3EOd3dbhyorkCdTEvRGcifIHDtm6FxTnled0f9dq6uj/YSm8
+l24OxYx2q3tl8h/VM6ZqJAcr7zbJv92tVoltzMq5OmfUq/ZufORARySCr7PV0s04
+7K8/bF/6ezOLDmKUl73Z7kYk5QlgUCUSBQAal8N2/qP+BNFi90kIki05ARC16S00
+IBmyhzTDAgMBAAECggEAVOdiElvLjyX6xeoC00YU6hxOIMdNtHU2HMamwtDV01UD
+38mMQ9KjrQelYt4n34drLrHe2IZw75/5J4JzagJrmUY47psHBwaDXItuZRokeJaw
+zhLYTEs7OcKRtV+a5WOspUrdzi33aQoFb67zZG3qkpsZyFXrdBV+/fy/Iv+MCvLH
+xR0jQ5mzE3cw20R7S4nddChBA/y8oKGOo6QRf2SznC1jL/+yolHvJPEn1v8AUxYm
+BMPHxj1O0c4M4IxnJQ3Y5Jy9OaFMyMsFlF1hVhc/3LDDxDyOuBsVsFDicojyrRea
+GKngIke0yezy7Wo4NUcp8YQhafonpWVsSJJdOUotcQKBgQD0rihFBXVtcG1d/Vy7
+FvLHrmccD56JNV744LSn2CDM7W1IulNbDUZINdCFqL91u5LpxozeE1FPY1nhwncJ
+N7V7XYCaSLCuV1YJzRmUCjnzk2RyopGpzWog3f9uUFGgrk1HGbNAv99k/REya6Iu
+IRSkuQhaJOj3bRXzonh0K4GjewKBgQDXvamtCioOUMSP8vq919YMkBw7F+z/fr0p
+pamO8HL9eewAUg6N92JQ9kobSo/GptdmdHIjs8LqnS5C3H13GX5Qlf5GskOlCpla
+V55ElaSp0gvKwWE168U7gQH4etPQAXXJrOGFaGbPj9W81hTUud7HVE88KYdfWTBo
+I7TuE25tWQKBgBRjcr2Vn9xXsvVTCGgamG5lLPhcoNREGz7X0pXt34XT/vhBdnKu
+331i5pZMom+YCrzqK5DRwUPBPpseTjb5amj2OKIijn5ojqXQbmI0m/GdBZC71TF2
+CXLlrMQvcy3VeGEFVjd+BYpvwAAYkfIQFZ1IQdbpHnSHpX2guzLK8UmDAoGBANUy
+PIcf0EetUVHfkCIjNQfdMcjD8BTcLhsF9vWmcDxFTA9VB8ULf0D64mjt2f85yQsa
+b+EQN8KZ6alxMxuLOeRxFYLPj0F9o+Y/R8wHBV48kCKhz2r1v0b6SfQ/jSm1B61x
+BrxLW64qOdIOzS8bLyhUDKkrcPesr8V548aRtUKhAoGBAKlNJFd8BCGKD9Td+3dE
+oP1iHTX5XZ+cQIqL0e+GMQlK4HnQP566DFZU5/GHNNAfmyxd5iSRwhTqPMHRAmOb
+pqQwsyufx0dFeIBxeSO3Z6jW5h2sl4nBipZpw9bzv6EBL1xRr0SfMNZzdnf4JFzc
+0htGo/VO93Z2pv8w7uGUz1nN
+-----END PRIVATE KEY-----
diff --git a/modules/transport-netty4/src/test/resources/netty4-secure.jks b/modules/transport-netty4/src/test/resources/netty4-secure.jks
new file mode 100644
index 0000000000000000000000000000000000000000..59dfd31c2a1567c6fbae386aa8f15c563bc66ae0
GIT binary patch
literal 2790
zcma)8XEYm(8crgiLaZ7k_^3S_Tdh)5I#8=<Yd1!XG-_5O8cM}z?NLRH(pvS|qgJR<
z4MMA=G<-&@W~`e1+;i{gckZuy&wJkUp67Yq=l%Jd7lDV)f`AMNJT#t>SvDFOy$b^}
z0rT-tEEo?BJ)r{;cu4DiNstOK9`fphE<R}@Bg=nWtWY2*9}m$#A$1V$za0=5!XBad
z4`fBifw@Z?UHUl{O6rb;hQ&>~rNH!<Z6**%iUR}`L9jBO`u9Z;GX#JTVq|uXMgma`
zV4y6RgYV#!_iDU*BS3Y?|CkxK2gZYkqhK(?8(KgzwJD3=OafGx>5#!!9K_}1H^u_V
z=usLU2dZ9W+tu&&HP&rr0Gj`32Xv({(IBcgf--PNjPP?Wl@c*PRlG~?w#y{$$E7Ac
zx~e{WW#twe_l9Rk?!G}q_9k~<RU*lxvB_tpaMk%8S@Gtc@6fax2mQ~N`~aB6kAc=t
zW9QCOZYA-^xAAqed>-h|^Nd;KI<0tW*OJCTydW5K?zK18ZR;_&+8)QRWs&D)HUeQu
z9uJg@R_z>@?>MI)N$iZs?C70IBKa$HMTmwtRZM2#Eoh>qRjH=pY|<sfxO;u(53&uU
zc6(e_JH<>48!KPy$z*ryQVZ6bbD&&t4TF&?ZKHwA0q_P!85hGFhoG?V+nrEvu|YdG
zNrL!x5hXK9XG!dHbKAhpa*yK2X+&`U9}I8`ON-SR2AaO}$md&6HEJY?sp&0?74^a&
zVGN_jRIazY8Xhs~^9fS?Fi-gwcl8p=<-=HX>-`)aSrru|SJ3LA(S5lX(T8(;a;TEk
zjAO5JZ;+6Ri7Fk+ojzl_j>FsNxien-@nif$*x?i|H~&{et(-zmOA(y|b9$qWo>F~6
zki+c@3Ez0iXk}m9oTR;sSPhbZ=SK~bRJ`f>5rk8+I0fjlFBn~o1e<4(<AxXNrP`tP
zPsu_Nq#>6(c5WFg`wy)X+ZN{sOnlx=ETJarFP_~xRzcNqSZ7%QHR!VzITeKkh$31~
zImEggf>lp$FXnU=Ts#bWExW*Y9;7%4Oe?((yn8vIeg2W0PfQBbZg?H7S6jR7_Ab`i
zFt+ORFZV!dxP5(el{8;w<@pHgN@T-AM|1c0R#EBHJ|EF9tDR?*`HCSSS-NChY<rOJ
z!o0%+l(0$4dcz=w5rg^4jHT680HE(VUv^*VdE0wWyGHZ?+v_s<g$pi~v9P|ke|mVq
z$xQ4tl!QbFcT%UCXKR)c2tqzV*KvUqHC%+&l(QZ$CNgBo0Nu=ylwW@s?CUV}Hm@XH
zOMaH1Ac<7EYFp>vG5g86>yVnk$g?6B#Mb(2mG-cf!r7rvHyV<s=&0nIm!Wss!%`ct
zBvMRNx3$)A^Ir+NoaQ7C5P+C?E?v*k?#w-Q@sLt2(GtLM+u}2qciX(o=?!sG#)Fd~
zf0`{z@0x^Y6?|eZ=F_Ipv&p>nf8E8TJzgSB$KPWTl~g|#-Rw(h>E!kcXTQ3Q6z`cP
z)fS*)cgRrG{5|EtiN~1M7vJ;J2n&CXaT*Pb--Bc$Zi!bnJlbwCvj3*7ebDIBSRV6)
zJ(Pz1F<m)TV^408#^o9(cv3Z1v;+ozbWVmIQ<IpIg)ODCi<L*}bH}>*1RFZ#H99b!
zFAr=1?f}O-Ds0l4sX~>=qET8%wZFXa-H{oyX`N12eqs0c0QyH-0D8xlm&3z#uIe`1
zrI9EslWc5kB75)nwQj9;_~TDgCgpG=C=?yr`DQL%YWUdLLKt~BF_>(ozPEDYfmq5|
z0JoEQ)AV!wN+o_w*8sM82I;8g<TpH(!z)xEav;p>UaTdYv=QY$QCfcLv(IxGSQ+S>
z%RxCJI*K1%Di#OIIA!I`gNrV;_(=luy&a6i<Po9^tDkrtT?{MgC&1XJKL8}_ck1s5
zsenc?;Ywx*ncs2A%&jCSECBEWpa20U6$(%VC;;xCAU8lDz#o7@i2k$6qr?s7vi9;t
zi706(Ayie>R5jIAl$8;9aP{vcCdPa`xZs4!1pxskBjX<h_`gCMwLWrpx-MoIQG~p9
zU5uyK@xxNi{}bAI8zP*~$$eXlN(&|x@awn;JXqxY$gk;48?nn)YE8QpAG6nt>CrY<
zOcTKv`EhagL&xGTrIIUt2v?a2%l?bgMOV&&+|WU;e$<xjTg$gyL?0zCRkibPsAd+F
z@fbhboH<AnoFa6_=*u_74CFVv$|ssu`*e;hUaDJ?SQGrxv7^Nv_K86bJ^6Uc;66nM
z7I2``a3Q3pJd3|^o!40{Q(<vF_6YH;vhbRSDL)VW={#~i3)$fA`nt|uUxBjEMifKh
zun;DX;(@Z8&VCcIXf3kKQXon<V{$uhv%pS#FvW69HP1gpQroaEHQ2m#-w`8K)$mF*
z@>=RpZpz1|dNV>*m~d#i)d2TYZVO)~XMlNxBrxoWuJ<66oc1(`c8LIHSqMFI@oVB5
zCTi-Btx2^*IwaoUOI*HGf^JQ#GU_^GnY6KL3-W;<6D`7ahRD>wHVTL3(9$baE*+Uh
zI?Qh*6kS<aVb(Bvgj+vnH20U6)eZ5|**?FU%60f|0#*y_y+Q90Ozc}}NB8u9Q|4U8
zQ+sJk4eZJNmTLmxC0@xlxa65#@YqoqV3)|xg~h=4M_m~r3kn<bOl8W?CrlOIq@Ky7
zzM0goHEr6amL?_HR?kP_`oXoYdF!oxBogG)z&>`O(EC~T8Xj=&-O3)}wcfX~t$yft
z0#%1s3udIR?zCsDgD^((_g@(9316i|*L)iFc0tc}Oh8!c)=u|})&8|LYjG)q`<fum
zjsFQ8$^<nJ6)a~TK$qBkP5M^H;J9gB9cy2zybwNd;cUG4GvZn$X^2fz)pp_B9my7p
zzBakrAF_O|ole@BgPjSfuidiPz`gcgPEmRT3|=jY8k1BcvhgIZKl`X9o~vV<@yply
zP|;XBxKN^W!<_-75RR<5k#dK%$@Jm2*eFlnc{#oFwAlKhnuVNp=eUhq!qe~<1+qQM
zEHAYq6XNz7*hq8e2VEV{Z7Q@`7hShpKQq*tQkWzI@X+*9au?eNMS*WYb+$KjKq^#x
znM3v1dN4RNz_&fi)-NJnZCi7#V~;E97!F5ArF|RUnan4l@~^B)MU(pB=Wp0hJ*udv
znDv!SGk3LB%@vf9bozs=ZrfK!wMP{f$={zL-PB(0x346^M4Lu>SIr(O$~<-27o6EP
z#<~$zeByPj(`59rPL~;qQy3J_7#W=XF0H@rb!E#Ub}O0BW;u<W%G2ukezz_b$dz{V
z#B8=9+I~|*rQ=PqO1f=8O<p)oLp4h99qLGSfrUqWTW8z1`%BH(exD}K@h~g9jd@#o
zBG{mG`YhXA(i^GnBwn?q+>}u1Ml#*1v|V#XY%_PSfaE#-vVR6s9p13SJS(vq?@w&x
z1io}4NWAA0VtOK@D-a#+({tI}OZlv_g9^ZdL~a$%7K051T_}A1FpPpLUh51%m?D%A
zu;0HQ2nYdyMN)q`vbBY0Z04-Vi-TU!{Ws&)J05FHCn{A|`7rGNj0AIUHubc-71}?2
T^$whsN!%rkGx+QOE291eC9)6<

literal 0
HcmV?d00001

diff --git a/server/src/main/java/org/opensearch/common/network/NetworkModule.java b/server/src/main/java/org/opensearch/common/network/NetworkModule.java
index 2edf3967c61b0..c7663f826c2ad 100644
--- a/server/src/main/java/org/opensearch/common/network/NetworkModule.java
+++ b/server/src/main/java/org/opensearch/common/network/NetworkModule.java
@@ -55,6 +55,7 @@
 import org.opensearch.http.HttpServerTransport;
 import org.opensearch.index.shard.PrimaryReplicaSyncer.ResyncTask;
 import org.opensearch.plugins.NetworkPlugin;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
 import org.opensearch.ratelimitting.admissioncontrol.enums.AdmissionControlActionType;
 import org.opensearch.tasks.RawTaskStatus;
 import org.opensearch.tasks.Task;
@@ -67,6 +68,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -85,6 +87,9 @@ public final class NetworkModule {
     public static final String HTTP_TYPE_KEY = "http.type";
     public static final String HTTP_TYPE_DEFAULT_KEY = "http.type.default";
     public static final String TRANSPORT_TYPE_DEFAULT_KEY = "transport.type.default";
+    public static final String TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY = "transport.ssl.enforce_hostname_verification";
+    public static final String TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY = "transport.ssl.resolve_hostname";
+    public static final String TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY = "transport.ssl.dual_mode.enabled";
 
     public static final Setting<String> TRANSPORT_DEFAULT_TYPE_SETTING = Setting.simpleString(
         TRANSPORT_TYPE_DEFAULT_KEY,
@@ -94,6 +99,22 @@ public final class NetworkModule {
     public static final Setting<String> HTTP_TYPE_SETTING = Setting.simpleString(HTTP_TYPE_KEY, Property.NodeScope);
     public static final Setting<String> TRANSPORT_TYPE_SETTING = Setting.simpleString(TRANSPORT_TYPE_KEY, Property.NodeScope);
 
+    public static final Setting<Boolean> TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION = Setting.boolSetting(
+        TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY,
+        true,
+        Property.NodeScope
+    );
+    public static final Setting<Boolean> TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = Setting.boolSetting(
+        TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY,
+        true,
+        Property.NodeScope
+    );
+    public static final Setting<Boolean> TRANSPORT_SSL_DUAL_MODE_ENABLED = Setting.boolSetting(
+        TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY,
+        false,
+        Property.NodeScope
+    );
+
     private final Settings settings;
 
     private static final List<NamedWriteableRegistry.Entry> namedWriteables = new ArrayList<>();
@@ -151,9 +172,17 @@ public NetworkModule(
         HttpServerTransport.Dispatcher dispatcher,
         ClusterSettings clusterSettings,
         Tracer tracer,
-        List<TransportInterceptor> transportInterceptors
+        List<TransportInterceptor> transportInterceptors,
+        Collection<SecureTransportSettingsProvider> secureTransportSettingsProvider
     ) {
         this.settings = settings;
+
+        if (secureTransportSettingsProvider.size() > 1) {
+            throw new IllegalArgumentException(
+                "there is more than one secure transport settings provider: " + secureTransportSettingsProvider
+            );
+        }
+
         for (NetworkPlugin plugin : plugins) {
             Map<String, Supplier<HttpServerTransport>> httpTransportFactory = plugin.getHttpTransports(
                 settings,
@@ -170,6 +199,7 @@ public NetworkModule(
             for (Map.Entry<String, Supplier<HttpServerTransport>> entry : httpTransportFactory.entrySet()) {
                 registerHttpTransport(entry.getKey(), entry.getValue());
             }
+
             Map<String, Supplier<Transport>> transportFactory = plugin.getTransports(
                 settings,
                 threadPool,
@@ -182,6 +212,43 @@ public NetworkModule(
             for (Map.Entry<String, Supplier<Transport>> entry : transportFactory.entrySet()) {
                 registerTransport(entry.getKey(), entry.getValue());
             }
+
+            // Register any secure transports if available
+            if (secureTransportSettingsProvider.isEmpty() == false) {
+                final SecureTransportSettingsProvider secureSettingProvider = secureTransportSettingsProvider.iterator().next();
+
+                final Map<String, Supplier<HttpServerTransport>> secureHttpTransportFactory = plugin.getSecureHttpTransports(
+                    settings,
+                    threadPool,
+                    bigArrays,
+                    pageCacheRecycler,
+                    circuitBreakerService,
+                    xContentRegistry,
+                    networkService,
+                    dispatcher,
+                    clusterSettings,
+                    secureSettingProvider,
+                    tracer
+                );
+                for (Map.Entry<String, Supplier<HttpServerTransport>> entry : secureHttpTransportFactory.entrySet()) {
+                    registerHttpTransport(entry.getKey(), entry.getValue());
+                }
+
+                final Map<String, Supplier<Transport>> secureTransportFactory = plugin.getSecureTransports(
+                    settings,
+                    threadPool,
+                    pageCacheRecycler,
+                    circuitBreakerService,
+                    namedWriteableRegistry,
+                    networkService,
+                    secureSettingProvider,
+                    tracer
+                );
+                for (Map.Entry<String, Supplier<Transport>> entry : secureTransportFactory.entrySet()) {
+                    registerTransport(entry.getKey(), entry.getValue());
+                }
+            }
+
             List<TransportInterceptor> pluginTransportInterceptors = plugin.getTransportInterceptors(
                 namedWriteableRegistry,
                 threadPool.getThreadContext()
diff --git a/server/src/main/java/org/opensearch/common/settings/ClusterSettings.java b/server/src/main/java/org/opensearch/common/settings/ClusterSettings.java
index 8bec0e04926a7..40e6ea9bec669 100644
--- a/server/src/main/java/org/opensearch/common/settings/ClusterSettings.java
+++ b/server/src/main/java/org/opensearch/common/settings/ClusterSettings.java
@@ -332,6 +332,9 @@ public void apply(Settings value, Settings current, Settings previous) {
                 NetworkModule.TRANSPORT_DEFAULT_TYPE_SETTING,
                 NetworkModule.HTTP_TYPE_SETTING,
                 NetworkModule.TRANSPORT_TYPE_SETTING,
+                NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED,
+                NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION,
+                NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME,
                 HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS,
                 HttpTransportSettings.SETTING_CORS_ENABLED,
                 HttpTransportSettings.SETTING_CORS_MAX_AGE,
diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java
index ed0daf2e6ed68..7fc057825d7d2 100644
--- a/server/src/main/java/org/opensearch/node/Node.java
+++ b/server/src/main/java/org/opensearch/node/Node.java
@@ -201,6 +201,7 @@
 import org.opensearch.plugins.ScriptPlugin;
 import org.opensearch.plugins.SearchPipelinePlugin;
 import org.opensearch.plugins.SearchPlugin;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
 import org.opensearch.plugins.SystemIndexPlugin;
 import org.opensearch.plugins.TelemetryPlugin;
 import org.opensearch.ratelimitting.admissioncontrol.AdmissionControlService;
@@ -942,6 +943,13 @@ protected Node(
                 admissionControlService
             );
 
+            final Collection<SecureTransportSettingsProvider> secureTransportSettingsProviders = pluginsService.filterPlugins(Plugin.class)
+                .stream()
+                .map(p -> p.getSecureSettingFactory(settings).flatMap(f -> f.getSecureTransportSettingsProvider(settings)))
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+
             List<TransportInterceptor> transportInterceptors = List.of(admissionControlTransportInterceptor);
             final NetworkModule networkModule = new NetworkModule(
                 settings,
@@ -956,7 +964,8 @@ protected Node(
                 restController,
                 clusterService.getClusterSettings(),
                 tracer,
-                transportInterceptors
+                transportInterceptors,
+                secureTransportSettingsProviders
             );
             Collection<UnaryOperator<Map<String, IndexTemplateMetadata>>> indexTemplateMetadataUpgraders = pluginsService.filterPlugins(
                 Plugin.class
diff --git a/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java b/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java
index 07df40bafe6a1..679833c9f6e0d 100644
--- a/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java
+++ b/server/src/main/java/org/opensearch/plugins/NetworkPlugin.java
@@ -107,4 +107,41 @@ default Map<String, Supplier<HttpServerTransport>> getHttpTransports(
     ) {
         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.
+     */
+    default Map<String, Supplier<Transport>> getSecureTransports(
+        Settings settings,
+        ThreadPool threadPool,
+        PageCacheRecycler pageCacheRecycler,
+        CircuitBreakerService circuitBreakerService,
+        NamedWriteableRegistry namedWriteableRegistry,
+        NetworkService networkService,
+        SecureTransportSettingsProvider secureTransportSettingsProvider,
+        Tracer tracer
+    ) {
+        return Collections.emptyMap();
+    }
+
+    /**
+     * Returns a map of secure {@link HttpServerTransport} suppliers.
+     * See {@link org.opensearch.common.network.NetworkModule#HTTP_TYPE_SETTING} to configure a specific implementation.
+     */
+    default Map<String, Supplier<HttpServerTransport>> getSecureHttpTransports(
+        Settings settings,
+        ThreadPool threadPool,
+        BigArrays bigArrays,
+        PageCacheRecycler pageCacheRecycler,
+        CircuitBreakerService circuitBreakerService,
+        NamedXContentRegistry xContentRegistry,
+        NetworkService networkService,
+        HttpServerTransport.Dispatcher dispatcher,
+        ClusterSettings clusterSettings,
+        SecureTransportSettingsProvider secureTransportSettingsProvider,
+        Tracer tracer
+    ) {
+        return Collections.emptyMap();
+    }
 }
diff --git a/server/src/main/java/org/opensearch/plugins/Plugin.java b/server/src/main/java/org/opensearch/plugins/Plugin.java
index 48486a6b55dfd..33c4155d12c25 100644
--- a/server/src/main/java/org/opensearch/plugins/Plugin.java
+++ b/server/src/main/java/org/opensearch/plugins/Plugin.java
@@ -269,4 +269,13 @@ public void close() throws IOException {
     public Collection<IndexSettingProvider> getAdditionalIndexSettingProviders() {
         return Collections.emptyList();
     }
+
+    /**
+     * Returns the {@link SecureSettingsFactory} instance that could be used to configure the
+     * security related components (fe. transports)
+     * @return the {@link SecureSettingsFactory} instance
+     */
+    public Optional<SecureSettingsFactory> getSecureSettingFactory(Settings settings) {
+        return Optional.empty();
+    }
 }
diff --git a/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java b/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java
new file mode 100644
index 0000000000000..b98d9cf51c129
--- /dev/null
+++ b/server/src/main/java/org/opensearch/plugins/SecureSettingsFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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 java.util.Optional;
+
+/**
+ * A factory for creating the instance of the {@link SecureTransportSettingsProvider}, taking into account current settings.
+ *
+ * @opensearch.experimental
+ */
+@ExperimentalApi
+public interface SecureSettingsFactory {
+    /**
+     * Creates (or provides pre-created) instance of the {@link SecureTransportSettingsProvider}
+     * @param settings settings
+     * @return optionally, the instance of the {@link SecureTransportSettingsProvider}
+     */
+    Optional<SecureTransportSettingsProvider> getSecureTransportSettingsProvider(Settings settings);
+}
diff --git a/server/src/main/java/org/opensearch/plugins/SecureTransportSettingsProvider.java b/server/src/main/java/org/opensearch/plugins/SecureTransportSettingsProvider.java
new file mode 100644
index 0000000000000..6d038ed30c8ff
--- /dev/null
+++ b/server/src/main/java/org/opensearch/plugins/SecureTransportSettingsProvider.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.opensearch.http.HttpServerTransport;
+import org.opensearch.transport.TcpTransport;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import java.util.Optional;
+
+/**
+ * A provider for security related settings for transports.
+ *
+ * @opensearch.experimental
+ */
+@ExperimentalApi
+public interface SecureTransportSettingsProvider {
+    /**
+     * An exception handler for errors that might happen while secure transport handle the requests.
+     *
+     * @see <a href="https://github.com/opensearch-project/security/blob/main/src/main/java/org/opensearch/security/ssl/SslExceptionHandler.java">SslExceptionHandler</a>
+     *
+     * @opensearch.experimental
+     */
+    @ExperimentalApi
+    @FunctionalInterface
+    interface ServerExceptionHandler {
+        static ServerExceptionHandler NOOP = t -> {};
+
+        /**
+         * Handler for errors happening during the server side processing of the requests
+         * @param t the error
+         */
+        void onError(Throwable t);
+    }
+
+    /**
+     * If supported, builds the {@link ServerExceptionHandler} instance for {@link HttpServerTransport} instance
+     * @param settings settings
+     * @param transport {@link HttpServerTransport} instance
+     * @return if supported, builds the {@link ServerExceptionHandler} instance
+     */
+    Optional<ServerExceptionHandler> buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport);
+
+    /**
+     * If supported, builds the {@link ServerExceptionHandler} instance for {@link TcpTransport} instance
+     * @param settings settings
+     * @param transport {@link TcpTransport} instance
+     * @return if supported, builds the {@link ServerExceptionHandler} instance
+     */
+    Optional<ServerExceptionHandler> buildServerTransportExceptionHandler(Settings settings, TcpTransport transport);
+
+    /**
+     * If supported, builds the {@link SSLEngine} instance for {@link HttpServerTransport} instance
+     * @param settings settings
+     * @param transport {@link HttpServerTransport} instance
+     * @return if supported, builds the {@link SSLEngine} instance
+     * @throws SSLException throws SSLException if the {@link SSLEngine} instance cannot be built
+     */
+    Optional<SSLEngine> buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException;
+
+    /**
+     * If supported, builds the {@link SSLEngine} instance for {@link TcpTransport} instance
+     * @param settings settings
+     * @param transport {@link TcpTransport} instance
+     * @return if supported, builds the {@link SSLEngine} instance
+     * @throws SSLException throws SSLException if the {@link SSLEngine} instance cannot be built
+     */
+    Optional<SSLEngine> buildSecureServerTransportEngine(Settings settings, TcpTransport transport) throws SSLException;
+
+    /**
+     * If supported, builds the {@link SSLEngine} instance for client transport instance
+     * @param settings settings
+     * @param hostname host name
+     * @param port port
+     * @return if supported, builds the {@link SSLEngine} instance
+     * @throws SSLException throws SSLException if the {@link SSLEngine} instance cannot be built
+     */
+    Optional<SSLEngine> buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException;
+}
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 de4bdcac6c2b2..1c607ca0dc98b 100644
--- a/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java
+++ b/server/src/test/java/org/opensearch/common/network/NetworkModuleTests.java
@@ -47,33 +47,66 @@
 import org.opensearch.http.HttpStats;
 import org.opensearch.http.NullDispatcher;
 import org.opensearch.plugins.NetworkPlugin;
+import org.opensearch.plugins.SecureTransportSettingsProvider;
 import org.opensearch.telemetry.tracing.Tracer;
 import org.opensearch.telemetry.tracing.noop.NoopTracer;
 import org.opensearch.test.OpenSearchTestCase;
 import org.opensearch.threadpool.TestThreadPool;
 import org.opensearch.threadpool.ThreadPool;
+import org.opensearch.transport.TcpTransport;
 import org.opensearch.transport.Transport;
 import org.opensearch.transport.TransportInterceptor;
 import org.opensearch.transport.TransportRequest;
 import org.opensearch.transport.TransportRequestHandler;
 
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
 
 public class NetworkModuleTests extends OpenSearchTestCase {
     private ThreadPool threadPool;
+    private SecureTransportSettingsProvider secureTransportSettingsProvider;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
         threadPool = new TestThreadPool(NetworkModuleTests.class.getName());
+        secureTransportSettingsProvider = new SecureTransportSettingsProvider() {
+            @Override
+            public Optional<ServerExceptionHandler> buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<ServerExceptionHandler> buildServerTransportExceptionHandler(Settings settings, TcpTransport transport) {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureServerTransportEngine(Settings settings, TcpTransport transport) throws SSLException {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<SSLEngine> buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException {
+                return Optional.empty();
+            }
+        };
     }
 
     @Override
@@ -160,6 +193,56 @@ public Map<String, Supplier<HttpServerTransport>> getHttpTransports(
         expectThrows(IllegalStateException.class, () -> newModule.getHttpServerTransportSupplier());
     }
 
+    public void testRegisterSecureTransport() {
+        Settings settings = Settings.builder().put(NetworkModule.TRANSPORT_TYPE_KEY, "custom-secure").build();
+        Supplier<Transport> custom = () -> null; // content doesn't matter we check reference equality
+        NetworkPlugin plugin = new NetworkPlugin() {
+            @Override
+            public Map<String, Supplier<Transport>> getSecureTransports(
+                Settings settings,
+                ThreadPool threadPool,
+                PageCacheRecycler pageCacheRecycler,
+                CircuitBreakerService circuitBreakerService,
+                NamedWriteableRegistry namedWriteableRegistry,
+                NetworkService networkService,
+                SecureTransportSettingsProvider secureTransportSettingsProvider,
+                Tracer tracer
+            ) {
+                return Collections.singletonMap("custom-secure", custom);
+            }
+        };
+        NetworkModule module = newNetworkModule(settings, null, List.of(secureTransportSettingsProvider), plugin);
+        assertSame(custom, module.getTransportSupplier());
+    }
+
+    public void testRegisterSecureHttpTransport() {
+        Settings settings = Settings.builder()
+            .put(NetworkModule.HTTP_TYPE_SETTING.getKey(), "custom-secure")
+            .put(NetworkModule.TRANSPORT_TYPE_KEY, "local")
+            .build();
+        Supplier<HttpServerTransport> custom = FakeHttpTransport::new;
+
+        NetworkModule module = newNetworkModule(settings, null, List.of(secureTransportSettingsProvider), new NetworkPlugin() {
+            @Override
+            public Map<String, Supplier<HttpServerTransport>> getSecureHttpTransports(
+                Settings settings,
+                ThreadPool threadPool,
+                BigArrays bigArrays,
+                PageCacheRecycler pageCacheRecycler,
+                CircuitBreakerService circuitBreakerService,
+                NamedXContentRegistry xContentRegistry,
+                NetworkService networkService,
+                HttpServerTransport.Dispatcher requestDispatcher,
+                ClusterSettings clusterSettings,
+                SecureTransportSettingsProvider secureTransportSettingsProvider,
+                Tracer tracer
+            ) {
+                return Collections.singletonMap("custom-secure", custom);
+            }
+        });
+        assertSame(custom, module.getHttpServerTransportSupplier());
+    }
+
     public void testOverrideDefault() {
         Settings settings = Settings.builder()
             .put(NetworkModule.HTTP_TYPE_SETTING.getKey(), "custom")
@@ -505,6 +588,15 @@ private NetworkModule newNetworkModule(
         Settings settings,
         List<TransportInterceptor> coreTransportInterceptors,
         NetworkPlugin... plugins
+    ) {
+        return newNetworkModule(settings, coreTransportInterceptors, List.of(), plugins);
+    }
+
+    private NetworkModule newNetworkModule(
+        Settings settings,
+        List<TransportInterceptor> coreTransportInterceptors,
+        List<SecureTransportSettingsProvider> secureTransportSettingsProviders,
+        NetworkPlugin... plugins
     ) {
         return new NetworkModule(
             settings,
@@ -519,7 +611,8 @@ private NetworkModule newNetworkModule(
             new NullDispatcher(),
             new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS),
             NoopTracer.INSTANCE,
-            coreTransportInterceptors
+            coreTransportInterceptors,
+            secureTransportSettingsProviders
         );
     }
 }