This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new bf8adfb50e feat: add optional TLS hostname verification for classic
remoting (#3164)
bf8adfb50e is described below
commit bf8adfb50e4cc4fb94ef60082df0a45efcad6973
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jun 27 10:06:51 2026 +0800
feat: add optional TLS hostname verification for classic remoting (#3164)
* feat: add optional TLS hostname verification for classic remoting
Motivation:
Classic remoting's ConfigSSLEngineProvider never called
setEndpointIdentificationAlgorithm, meaning TLS certificate hostname
verification was not performed. An attacker with any certificate signed
by a trusted CA could perform MITM attacks. Artery already supports
hostname verification via a configurable option.
Modification:
- SSLSettings: add hostname-verification config option (default off)
- SSLEngineProvider trait: add createClientSSLEngine(hostname, port)
default method for backward compatibility
- ConfigSSLEngineProvider: implement hostname-aware engine creation with
setEndpointIdentificationAlgorithm("HTTPS") when enabled
- NettyTransport: pass remote address hostname to SSL handler on client
side when hostname verification is enabled
- NettySSLSupport: accept optional hostname/port parameters
Result:
Classic remoting can now optionally verify TLS certificate hostnames,
matching Artery's behavior. Disabled by default for backward
compatibility. Enable with hostname-verification = on in SSL security
config.
Tests:
- remote / Test / testOnly org.apache.pekko.remote.Ticket1978ConfigSpec
References:
Fixes #3161
* fix: address review - add hostname-verification to reference.conf and
security warning log
- Add hostname-verification config entry with documentation in
reference.conf for discoverability
- Add security info log when hostname verification is disabled,
matching Artery's behavior
---
remote/src/main/resources/reference.conf | 5 +++
.../remote/transport/netty/NettySSLSupport.scala | 18 +++++++--
.../remote/transport/netty/NettyTransport.scala | 13 ++++--
.../remote/transport/netty/SSLEngineProvider.scala | 46 ++++++++++++++++++----
.../apache/pekko/remote/Ticket1978ConfigSpec.scala | 10 +++++
5 files changed, 79 insertions(+), 13 deletions(-)
diff --git a/remote/src/main/resources/reference.conf
b/remote/src/main/resources/reference.conf
index ab71456d3c..c1ecc59fc0 100644
--- a/remote/src/main/resources/reference.conf
+++ b/remote/src/main/resources/reference.conf
@@ -733,6 +733,11 @@ pekko {
#
# To prevent man-in-the-middle attacks this setting is enabled by
default.
require-mutual-authentication = on
+
+ # Set this to `on` to verify the hostname in the server's TLS
certificate
+ # against the hostname used to connect. When `off`, any certificate
signed
+ # by a trusted CA will be accepted, which is vulnerable to MITM
attacks.
+ hostname-verification = off
}
}
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
index 06d723655e..2b6cc31cc2 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettySSLSupport.scala
@@ -49,6 +49,9 @@ private[pekko] class SSLSettings(config: Config) {
val SSLRequireMutualAuthentication =
getBoolean("require-mutual-authentication")
+ val SSLHostnameVerification: Boolean =
+ if (config.hasPath("hostname-verification"))
config.getBoolean("hostname-verification")
+ else false
}
/**
@@ -63,10 +66,19 @@ private[pekko] object NettySSLSupport {
/**
* Construct a SSLHandler which can be inserted into a Netty server/client
pipeline
*/
- def apply(sslEngineProvider: SSLEngineProvider, isClient: Boolean, log:
MarkerLoggingAdapter): SslHandler = {
+ def apply(
+ sslEngineProvider: SSLEngineProvider,
+ isClient: Boolean,
+ log: MarkerLoggingAdapter,
+ hostname: Option[String] = None,
+ port: Int = -1): SslHandler = {
val sslEngine =
- if (isClient) sslEngineProvider.createClientSSLEngine()
- else sslEngineProvider.createServerSSLEngine()
+ if (isClient) {
+ hostname match {
+ case Some(h) => sslEngineProvider.createClientSSLEngine(h, port)
+ case None => sslEngineProvider.createClientSSLEngine()
+ }
+ } else sslEngineProvider.createServerSSLEngine()
val handler = new SslHandler(sslEngine)
handler.handshakeFuture().addListener((future: Future[Channel]) => {
if (!future.isSuccess) {
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
index bba302ce03..28005094a9 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/NettyTransport.scala
@@ -420,10 +420,17 @@ class NettyTransport(val settings:
NettyTransportSettings, val system: ExtendedA
.get)
} else OptionVal.None
- private def sslHandler(isClient: Boolean): SslHandler = {
+ private def sslHandler(isClient: Boolean, remoteAddress: Option[Address] =
None): SslHandler = {
sslEngineProvider match {
case OptionVal.Some(sslProvider) =>
- NettySSLSupport(sslProvider, isClient, log)
+ val hostnameVerification =
SslSettings.exists(_.SSLHostnameVerification)
+ if (isClient && hostnameVerification && remoteAddress.isDefined) {
+ val addr = remoteAddress.get
+ val port = addr.port.getOrElse(-1)
+ NettySSLSupport(sslProvider, isClient, log, addr.host, port)
+ } else {
+ NettySSLSupport(sslProvider, isClient, log)
+ }
case _ =>
throw new IllegalStateException("Expected enable-ssl=on")
}
@@ -440,7 +447,7 @@ class NettyTransport(val settings: NettyTransportSettings,
val system: ExtendedA
private def clientPipelineInitializer(remoteAddress: Address):
ChannelInitializer[SocketChannel] =
(ch: SocketChannel) => {
val pipeline = newPipeline(ch)
- if (EnableSsl) pipeline.addFirst("SslHandler", sslHandler(isClient =
true))
+ if (EnableSsl) pipeline.addFirst("SslHandler", sslHandler(isClient =
true, Some(remoteAddress)))
val handler = new TcpClientHandler(NettyTransport.this, remoteAddress,
log)
pipeline.addLast("clienthandler", handler)
}
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
index 88619cb8fa..39a7bd3752 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/transport/netty/SSLEngineProvider.scala
@@ -27,10 +27,12 @@ import javax.net.ssl.SSLEngine
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
+import scala.annotation.nowarn
import scala.util.Try
import org.apache.pekko
import pekko.actor.ActorSystem
+import pekko.event.LogMarker
import pekko.event.Logging
import pekko.event.MarkerLoggingAdapter
import pekko.remote.RemoteTransportException
@@ -44,6 +46,13 @@ trait SSLEngineProvider {
def createClientSSLEngine(): SSLEngine
+ /**
+ * Create a client SSLEngine with the target hostname and port for hostname
verification.
+ * Default implementation delegates to [[createClientSSLEngine()]].
+ */
+ @nowarn("msg=never used")
+ def createClientSSLEngine(hostname: String, port: Int): SSLEngine =
createClientSSLEngine()
+
}
/**
@@ -62,7 +71,15 @@ class ConfigSSLEngineProvider(protected val log:
MarkerLoggingAdapter, private v
import settings._
- private val sslContext: SSLContext = {
+ if (SSLHostnameVerification)
+ log.debug("TLS/SSL hostname verification is enabled.")
+ else
+ log.info(
+ LogMarker.Security,
+ "TLS/SSL hostname verification is disabled. " +
+ "Set pekko.remote.classic.netty.ssl.security.hostname-verification=on to
enable.")
+
+ private lazy val sslContext: SSLContext = {
try {
val rng = createSecureRandom()
val ctx = SSLContext.getInstance(SSLProtocol)
@@ -115,18 +132,33 @@ class ConfigSSLEngineProvider(protected val log:
MarkerLoggingAdapter, private v
SecureRandomFactory.createSecureRandom(SSLRandomNumberGenerator, log)
override def createServerSSLEngine(): SSLEngine =
- createSSLEngine(pekko.stream.Server)
+ createSSLEngine(pekko.stream.Server, None)
override def createClientSSLEngine(): SSLEngine =
- createSSLEngine(pekko.stream.Client)
+ createSSLEngine(pekko.stream.Client, None)
- private def createSSLEngine(role: TLSRole): SSLEngine = {
- createSSLEngine(sslContext, role)
+ override def createClientSSLEngine(hostname: String, port: Int): SSLEngine =
+ createSSLEngine(pekko.stream.Client, Some((hostname, port)))
+
+ private def createSSLEngine(role: TLSRole, peerHost: Option[(String, Int)]):
SSLEngine = {
+ createSSLEngine(sslContext, role, peerHost)
}
- private def createSSLEngine(sslContext: SSLContext, role: TLSRole):
SSLEngine = {
+ private def createSSLEngine(
+ sslContext: SSLContext,
+ role: TLSRole,
+ peerHost: Option[(String, Int)]): SSLEngine = {
+
+ val engine = peerHost match {
+ case Some((hostname, port)) => sslContext.createSSLEngine(hostname, port)
+ case None => sslContext.createSSLEngine()
+ }
- val engine = sslContext.createSSLEngine()
+ if (SSLHostnameVerification && role == pekko.stream.Client) {
+ val sslParams = sslContext.getDefaultSSLParameters
+ sslParams.setEndpointIdentificationAlgorithm("HTTPS")
+ engine.setSSLParameters(sslParams)
+ }
engine.setUseClientMode(role == pekko.stream.Client)
engine.setEnabledCipherSuites(SSLEnabledAlgorithms.toArray)
diff --git
a/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
b/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
index 2fc434d6c2..b7cb9934b4 100644
--- a/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
+++ b/remote/src/test/scala/org/apache/pekko/remote/Ticket1978ConfigSpec.scala
@@ -36,6 +36,16 @@ class Ticket1978ConfigSpec extends PekkoSpec("""
settings.SSLEnabledAlgorithms should ===(
Set("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"))
settings.SSLRandomNumberGenerator should ===("SecureRandom")
+ settings.SSLHostnameVerification should ===(false)
+ }
+
+ "support hostname-verification setting" in {
+ val settings = new SSLSettings(
+ com.typesafe.config.ConfigFactory
+ .parseString("hostname-verification = on")
+
.withFallback(system.settings.config.getConfig("pekko.remote.classic.netty.ssl.security")))
+
+ settings.SSLHostnameVerification should ===(true)
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]