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]