This is an automated email from the ASF dual-hosted git repository.

pjfanning 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 0546b2cdc6 docs: document post-handshake session verification for TLS 
(#3462)
0546b2cdc6 is described below

commit 0546b2cdc672bcecc526fd34260f1a66685038c0
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Aug 28 07:31:24 2026 +0100

    docs: document post-handshake session verification for TLS (#3462)
    
    The `SSLEngineProvider` trait exposes `verifyClientSession` and
    `verifyServerSession`, which Artery invokes after every successful TLS
    handshake and which fail the connection when they return `Some(cause)`.
    This extension point was only described in the context of
    `RotatingKeysSSLEngineProvider`, so users on the default JKS-based
    `ConfigSSLEngineProvider` had no signpost that overriding these methods
    is supported.
    
    Add a section to remote-security.md covering the callbacks, when to
    prefer `hostname-verification=on` instead, the interaction with
    `require-mutual-authentication`, and a compiled example that scopes a
    cluster to a subset of certificates issued by a shared internal CA.
---
 docs/src/main/paradox/remote-security.md           | 52 +++++++++++++++++++
 .../docs/remoting/SSLEngineProviderDocSpec.scala   | 58 ++++++++++++++++++++++
 2 files changed, 110 insertions(+)

diff --git a/docs/src/main/paradox/remote-security.md 
b/docs/src/main/paradox/remote-security.md
index 04fe82e80b..e400327f67 100644
--- a/docs/src/main/paradox/remote-security.md
+++ b/docs/src/main/paradox/remote-security.md
@@ -121,6 +121,58 @@ You have a few choices how to set up certificates and 
hostname verification:
     * If keys/certificates are stolen, only the same node can access the 
cluster (unless DNS is tampered with as well).
       You can revoke single certificates.
 
+### Custom post-handshake session verification
+
+After every successful TLS handshake, Pekko calls back into the configured 
`SSLEngineProvider` to allow additional
+verification of the session:
+
+```
+def verifyClientSession(hostname: String, session: SSLSession): 
Option[Throwable]
+def verifyServerSession(hostname: String, session: SSLSession): 
Option[Throwable]
+```
+
+Returning `None` accepts the session. Returning `Some(cause)` rejects it and 
the connection is failed with that cause.
+`verifyClientSession` is called on the side that initiated the connection, 
`verifyServerSession` on the side that
+accepted it.
+
+The default `ConfigSSLEngineProvider` accepts every session that completed the 
handshake, because the certificate chain
+has already been validated against the configured trust-store, and 
`hostname-verification` covers the usual case of
+checking that you reached the host you expected. Prefer 
`hostname-verification=on` over a custom verifier whenever the
+peer hostnames are known up front.
+
+A verifier is useful for authorization checks the trust-store cannot express. 
A common example is a shared internal CA:
+every node in the organisation holds a certificate signed by the same CA, so 
the trust-store accepts all of them, but
+only a subset should be allowed into this particular cluster. Subclass 
`ConfigSSLEngineProvider` and inspect the peer
+certificate:
+
+@@snip 
[SSLEngineProviderDocSpec.scala](/docs/src/test/scala/docs/remoting/SSLEngineProviderDocSpec.scala)
 { #ssl-engine-provider-session-verification }
+
+The provider is selected by class name, and the constructor taking a single 
`ActorSystem` is the one Pekko uses:
+
+```
+pekko.remote.artery {
+  transport = tls-tcp
+  ssl.ssl-engine-provider = "docs.remoting.ClusterScopedSSLEngineProvider"
+}
+```
+
+Requiring mutual authentication is what makes these checks meaningful on the 
accepting side. With
+`require-mutual-authentication = on` (the default) the accepting side requests 
a certificate from the connecting peer,
+so the peer certificates are available in `verifyServerSession`. If mutual 
authentication is disabled, the connecting
+peer presents no certificate and `getPeerCertificates` throws 
`SSLPeerUnverifiedException`.
+
+@@@ note
+
+Both methods are invoked on every handshake, so keep them cheap and 
non-blocking. Inspect only the already-parsed
+`SSLSession`; do not perform I/O such as CRL or OCSP lookups inline.
+
+@@@
+
+The built-in `RotatingKeysSSLEngineProvider` uses this same mechanism: it 
deliberately does not use
+`hostname-verification`, and instead verifies after the handshake that the 
peer certificate shares at least one subject
+name (CN or SAN) with its own certificate. See
+@ref:[mTLS with rotated certificates in 
Kubernetes](#mtls-with-rotated-certificates-in-kubernetes).
+
 See also a description of the settings in the @ref:[Remote 
Configuration](remoting-artery.md#remote-configuration-artery)
 section.
 
diff --git a/docs/src/test/scala/docs/remoting/SSLEngineProviderDocSpec.scala 
b/docs/src/test/scala/docs/remoting/SSLEngineProviderDocSpec.scala
new file mode 100644
index 0000000000..2875f97528
--- /dev/null
+++ b/docs/src/test/scala/docs/remoting/SSLEngineProviderDocSpec.scala
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package docs.remoting
+
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLSession
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.event.Logging
+import pekko.event.MarkerLoggingAdapter
+import pekko.remote.artery.tcp.ConfigSSLEngineProvider
+
+import com.typesafe.config.Config
+
+// #ssl-engine-provider-session-verification
+class ClusterScopedSSLEngineProvider(config: Config, log: MarkerLoggingAdapter)
+    extends ConfigSSLEngineProvider(config, log) {
+
+  def this(system: ActorSystem) =
+    this(
+      
system.settings.config.getConfig("pekko.remote.artery.ssl.config-ssl-engine"),
+      Logging.withMarker(system, 
classOf[ClusterScopedSSLEngineProvider].getName))
+
+  private val allowedNames = Set("my-service")
+
+  private def verify(session: SSLSession): Option[Throwable] =
+    session.getPeerCertificates.headOption match {
+      case Some(x509: X509Certificate) =>
+        val subject = x509.getSubjectX500Principal.getName
+        if (allowedNames.exists(subject.contains)) None
+        else Some(new IllegalArgumentException(s"Peer [$subject] is not 
allowed to join this cluster"))
+      case _ =>
+        Some(new IllegalArgumentException("No X.509 peer certificate 
presented"))
+    }
+
+  override def verifyClientSession(hostname: String, session: SSLSession): 
Option[Throwable] =
+    verify(session)
+
+  override def verifyServerSession(hostname: String, session: SSLSession): 
Option[Throwable] =
+    verify(session)
+}
+// #ssl-engine-provider-session-verification


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to