From 1183b2beac285302b3ee3b9417b22225d481bd31 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Wed, 24 Sep 2025 23:47:22 +0700 Subject: [PATCH 1/5] Fix TLS handshake error handling --- ...eAPISpec.ApproveRemote.DotNet.verified.txt | 1 + ...CoreAPISpec.ApproveRemote.Net.verified.txt | 1 + .../DotNettyTlsHandshakeFailureSpec.cs | 222 +++++++++++++++++- .../Akka.Remote/Configuration/Remote.conf | 12 +- src/core/Akka.Remote/Endpoint.cs | 32 +++ src/core/Akka.Remote/EndpointManager.cs | 5 + .../Transport/DotNetty/DotNettyTransport.cs | 72 +++++- .../DotNetty/DotNettyTransportSettings.cs | 42 +++- .../Transport/DotNetty/TcpTransport.cs | 20 +- src/core/Akka.Remote/Transport/Transport.cs | 3 +- 10 files changed, 393 insertions(+), 17 deletions(-) diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt index bcc6ce10a27..622d04dd8ac 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt @@ -598,6 +598,7 @@ namespace Akka.Remote.Transport Unknown = 0, Shutdown = 1, Quarantined = 2, + TlsHandshakeError = 3, } public sealed class DisassociateUnderlying : Akka.Remote.Transport.TransportOperation, Akka.Event.IDeadLetterSuppression { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt index 5ae71f05f74..7bcf63e17ae 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt @@ -598,6 +598,7 @@ namespace Akka.Remote.Transport Unknown = 0, Shutdown = 1, Quarantined = 2, + TlsHandshakeError = 3, } public sealed class DisassociateUnderlying : Akka.Remote.Transport.TransportOperation, Akka.Event.IDeadLetterSuppression { diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs index ca4a9a1e684..1a7efb3009d 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs @@ -12,6 +12,7 @@ using Akka.Actor; using Akka.Configuration; using Akka.TestKit; +using Akka.Event; using Xunit; using Xunit.Abstractions; @@ -27,11 +28,12 @@ public DotNettyTlsHandshakeFailureSpec(ITestOutputHelper output) : base(Configur { } - private static Config CreateConfig(bool enableSsl, string certPath, string certPassword, bool suppressValidation = true) + private static Config CreateConfig(bool enableSsl, string certPath, string certPassword, bool suppressValidation = true, bool requireClientCert = false, bool sendClientCert = true) { var baseConfig = ConfigurationFactory.ParseString(@"akka { loglevel = DEBUG actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" + remote.retry-gate-closed-for = 3s remote.dot-netty.tcp { port = 0 hostname = ""127.0.0.1"" @@ -46,6 +48,8 @@ private static Config CreateConfig(bool enableSsl, string certPath, string certP var escapedPath = certPath.Replace("\\", "\\\\"); var ssl = $@"akka.remote.dot-netty.tcp.ssl {{ suppress-validation = {(suppressValidation ? "on" : "off")} + require-client-certificate = {(requireClientCert ? "on" : "off")} + send-client-certificate = {(sendClientCert ? "on" : "off")} certificate {{ path = ""{escapedPath}"" password = ""{certPassword ?? string.Empty}"" @@ -65,7 +69,33 @@ private static void CreateCertificateWithoutPrivateKey() } [Fact] - public async Task Tls_handshake_failure_should_be_logged_and_detected() + public void Server_should_fail_fast_when_server_certificate_has_no_private_key() + { + CreateCertificateWithoutPrivateKey(); + + try + { + var baseCfg = CreateConfig(true, NoKeyCertPath, null, suppressValidation: true); + var failfast = ConfigurationFactory.ParseString(@"akka.remote.dot-netty.tcp.ssl.fail-fast-invalid-server-certificate = on"); + var serverConfig = baseCfg.WithFallback(failfast); + + Assert.ThrowsAny(() => + { + using var _ = ActorSystem.Create("ServerSystem", serverConfig); + }); + } + finally + { + try + { + if (File.Exists(NoKeyCertPath)) File.Delete(NoKeyCertPath); + } + catch { /* ignore */ } + } + } + + [Fact] + public async Task Tls_handshake_failure_should_be_logged_and_shutdown_server() { CreateCertificateWithoutPrivateKey(); @@ -103,6 +133,13 @@ public async Task Tls_handshake_failure_should_be_logged_and_detected() var err = errorProbe.ExpectMsg(TimeSpan.FromSeconds(10)); var msg = err.ToString(); Assert.Contains("TLS handshake failed", msg, StringComparison.OrdinalIgnoreCase); + + // Server should shutdown due to TLS failure + await AwaitAssertAsync(async () => + { + Assert.True(server.WhenTerminated.IsCompleted); + await Task.CompletedTask; + }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); } finally { @@ -119,6 +156,187 @@ public async Task Tls_handshake_failure_should_be_logged_and_detected() await Task.CompletedTask; } + [Fact] + public async Task Server_side_tls_handshake_failure_should_shutdown_server() + { + CreateCertificateWithoutPrivateKey(); + + ActorSystem server = null; + ActorSystem client = null; + + try + { + // Server with invalid server cert (no private key) -> server TLS handshake fails + var serverConfig = CreateConfig(true, NoKeyCertPath, null, suppressValidation: true); + server = ActorSystem.Create("ServerSystem", serverConfig); + InitializeLogger(server, "[SERVER] "); + + // Client with valid cert + var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true); + client = ActorSystem.Create("ClientSystem", clientConfig); + InitializeLogger(client, "[CLIENT] "); + + // Echo actor on server and client + var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); + var clientEcho = client.ActorOf(Props.Create(() => new EchoActor()), "echo"); + + var serverAddr = RARP.For(server).Provider.DefaultAddress; + var clientAddr = RARP.For(client).Provider.DefaultAddress; + + var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; + var clientEchoPath = new RootActorPath(clientAddr) / "user" / "echo"; + + // Subscribe to server errors to ensure TLS handshake failure is observed + var serverErrorProbe = CreateTestProbe(server); + server.EventStream.Subscribe(serverErrorProbe.Ref, typeof(Event.Error)); + + // Trigger inbound handshake failure on server: client tries to talk to server + var clientProbe = CreateTestProbe(client); + client.ActorSelection(serverEchoPath).Tell("ping", clientProbe.Ref); + + // Expect server to log TLS handshake failure promptly + var err = await serverErrorProbe.ExpectMsgAsync(TimeSpan.FromSeconds(10)); + Assert.Contains("TLS handshake failed", err.ToString(), StringComparison.OrdinalIgnoreCase); + + // Server should shutdown due to TLS failure + await AwaitAssertAsync(async () => + { + Assert.True(server.WhenTerminated.IsCompleted); + await Task.CompletedTask; + }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); + } + finally + { + if (client != null) + Shutdown(client, TimeSpan.FromSeconds(10)); + if (server != null) + Shutdown(server, TimeSpan.FromSeconds(10)); + try + { + if (File.Exists(NoKeyCertPath)) + File.Delete(NoKeyCertPath); + } + catch { /* ignore */ } + } + } + + [Fact] + public async Task Client_side_tls_handshake_failure_should_shutdown_client() + { + // Server has valid cert; client enforces validation so it should reject the self-signed server cert + ActorSystem server = null; + ActorSystem client = null; + + try + { + var serverConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true); + server = ActorSystem.Create("ServerSystem", serverConfig); + InitializeLogger(server, "[SERVER] "); + + var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: false); + client = ActorSystem.Create("ClientSystem", clientConfig); + InitializeLogger(client, "[CLIENT] "); + + var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); + + var serverAddr = RARP.For(server).Provider.DefaultAddress; + var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; + + // Trigger TLS handshake failure during association + client.ActorSelection(serverEchoPath).Tell("hello"); + + // Client should shutdown due to TLS failure + await AwaitAssertAsync(async () => + { + Assert.True(client.WhenTerminated.IsCompleted); + await Task.CompletedTask; + }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200)); + } + finally + { + if (client != null) + Shutdown(client, TimeSpan.FromSeconds(10)); + if (server != null) + Shutdown(server, TimeSpan.FromSeconds(10)); + } + } + + [Fact] + public async Task MutualTLS_should_succeed_when_client_certificate_is_required_and_provided() + { + ActorSystem server = null; + ActorSystem client = null; + + try + { + // Server requires client certificate but suppresses validation (self-signed ok) + var serverConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: true); + server = ActorSystem.Create("ServerSystem", serverConfig); + InitializeLogger(server, "[SERVER] "); + + // Client sends client certificate + var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: false, sendClientCert: true); + client = ActorSystem.Create("ClientSystem", clientConfig); + InitializeLogger(client, "[CLIENT] "); + + var echo = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); + var serverAddr = RARP.For(server).Provider.DefaultAddress; + var echoPath = new RootActorPath(serverAddr) / "user" / "echo"; + + var probe = CreateTestProbe(client); + await AwaitAssertAsync(async () => + { + client.ActorSelection(echoPath).Tell("mtls-ok", probe.Ref); + await probe.ExpectMsgAsync("mtls-ok", TimeSpan.FromSeconds(1)); + }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200)); + } + finally + { + if (client != null) Shutdown(client, TimeSpan.FromSeconds(10)); + if (server != null) Shutdown(server, TimeSpan.FromSeconds(10)); + } + } + + [Fact] + public async Task MutualTLS_should_shutdown_when_client_certificate_is_required_but_not_provided() + { + ActorSystem server = null; + ActorSystem client = null; + + try + { + // Server requires client certificate, suppress validation for self-signed + var serverConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: false, requireClientCert: true); + server = ActorSystem.Create("ServerSystem", serverConfig); + InitializeLogger(server, "[SERVER] "); + + // Client does NOT send client certificate + var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: false, sendClientCert: false); + client = ActorSystem.Create("ClientSystem", clientConfig); + InitializeLogger(client, "[CLIENT] "); + + // Create echo on server + var echo = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); + + var serverAddr = RARP.For(server).Provider.DefaultAddress; + var echoPath = new RootActorPath(serverAddr) / "user" / "echo"; + + // Attempt communication; server should shutdown due to TLS failure (client cert required but not provided) + client.ActorSelection(echoPath).Tell("should-fail"); + + await AwaitAssertAsync(async () => + { + Assert.True(server.WhenTerminated.IsCompleted); + await Task.CompletedTask; + }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200)); + } + finally + { + if (client != null) Shutdown(client, TimeSpan.FromSeconds(10)); + if (server != null) Shutdown(server, TimeSpan.FromSeconds(10)); + } + } + private sealed class EchoActor : ReceiveActor { public EchoActor() diff --git a/src/core/Akka.Remote/Configuration/Remote.conf b/src/core/Akka.Remote/Configuration/Remote.conf index 9da126e1fde..7f5ad1eecd8 100644 --- a/src/core/Akka.Remote/Configuration/Remote.conf +++ b/src/core/Akka.Remote/Configuration/Remote.conf @@ -523,6 +523,16 @@ akka { } ssl { + # When enabled, server will require client certificate during TLS handshake (mutual TLS) + require-client-certificate = off + + # When enabled, client will present its certificate during TLS handshake (if available) + send-client-certificate = on + + # When enabled, remoting will fail fast at startup if configured server certificate is unusable + # (e.g. missing private key or lacking Server Authentication EKU). + fail-fast-invalid-server-certificate = off + certificate { # Valid certificate path required path = "" @@ -589,4 +599,4 @@ akka { channel-executor.priority = "low" } } -} \ No newline at end of file +} diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index a195dd1119b..02fc5f82293 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -209,6 +209,36 @@ protected EndpointException(SerializationInfo info, StreamingContext context) /// internal interface IAssociationProblem { } + /// + /// INTERNAL API + /// + internal sealed class TlsHandshakeErrorAssociation : EndpointException, IAssociationProblem + { + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public TlsHandshakeErrorAssociation(string message, Address localAddress, Address remoteAddress, Exception cause = null) + : base(message, cause) + { + RemoteAddress = remoteAddress; + LocalAddress = localAddress; + } + + /// + /// TBD + /// + public Address LocalAddress { get; private set; } + + /// + /// TBD + /// + public Address RemoteAddress { get; private set; } + } + /// /// INTERNAL API /// @@ -1940,6 +1970,8 @@ private void HandleDisassociated(DisassociateInfo info) "to the remote system are possible until this system is restarted.", LocalAddress, RemoteAddress, disassociateInfo: DisassociateInfo.Quarantined); case DisassociateInfo.Shutdown: throw new ShutDownAssociation($"The remote system terminated the association because it is shutting down. Shut down address: {RemoteAddress}", LocalAddress, RemoteAddress); + case DisassociateInfo.TlsHandshakeError: + throw new TlsHandshakeErrorAssociation("TLS handshake failed.", LocalAddress, RemoteAddress); case DisassociateInfo.Unknown: default: Context.Stop(Self); diff --git a/src/core/Akka.Remote/EndpointManager.cs b/src/core/Akka.Remote/EndpointManager.cs index a9ce57581e1..802cecd8893 100644 --- a/src/core/Akka.Remote/EndpointManager.cs +++ b/src/core/Akka.Remote/EndpointManager.cs @@ -14,6 +14,7 @@ using Akka.Actor; using Akka.Configuration; using Akka.Dispatch; +using Akka.Remote.Transport.DotNetty; using Akka.Event; using Akka.Remote.Transport; using Akka.Util.Internal; @@ -569,6 +570,10 @@ Directive Hopeless(HopelessAssociation e) switch (ex) { + case TlsHandshakeErrorAssociation tls: + _log.Error("Shutting down ActorSystem due to TLS handshake failure between [{0}] and [{1}]", tls.LocalAddress, tls.RemoteAddress); + CoordinatedShutdown.Get(Context.System).Run(new TlsHandshakeFailureReason($"TLS handshake failure between [{tls.LocalAddress}] and [{tls.RemoteAddress}]")); + return Directive.Stop; case InvalidAssociation ia: KeepQuarantinedOr(ia.RemoteAddress, () => { diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index c62da27b042..d24bd83a8f5 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -149,6 +149,43 @@ protected DotNettyTransport(ActorSystem system, Config config) AssociationListenerPromise = new TaskCompletionSource(); SchemeIdentifier = (Settings.EnableSsl ? "ssl." : string.Empty) + Settings.TransportMode.ToString().ToLowerInvariant(); + + if (Settings.EnableSsl && Settings.Ssl.Certificate != null && Settings.Ssl.FailFastInvalidServerCertificate) + { + ValidateServerCertificate(Settings.Ssl.Certificate); + } + } + + private static void ValidateServerCertificate(X509Certificate2 certificate) + { + if (!certificate.HasPrivateKey) + throw new ConfigurationException("Configured SSL certificate is missing a private key and cannot be used for server authentication."); + + try + { + var eku = certificate.Extensions.OfType().FirstOrDefault(); + if (eku != null) + { + var hasServerAuth = eku.EnhancedKeyUsages + .Cast() + .Any(o => string.Equals(o.Value, "1.3.6.1.5.5.7.3.1", StringComparison.Ordinal)); + if (!hasServerAuth) + throw new ConfigurationException("Configured SSL certificate lacks 'Server Authentication' EKU (1.3.6.1.5.5.7.3.1)."); + } + + // Ensure we can access a private key handle + using var _ = certificate.GetRSAPrivateKey() as IDisposable ?? certificate.GetECDsaPrivateKey() as IDisposable; + if (_ == null) + throw new ConfigurationException("Configured SSL certificate does not expose an accessible RSA/ECDSA private key."); + } + catch (ConfigurationException) + { + throw; + } + catch (Exception ex) + { + throw new ConfigurationException("Configured SSL certificate validation failed.", ex); + } } public DotNettyTransportSettings Settings { get; } @@ -347,9 +384,22 @@ private void SetClientPipeline(IChannel channel, Address remoteAddress) var certificate = Settings.Ssl.Certificate; var host = certificate.GetNameInfo(X509NameType.DnsName, false); - var tlsHandler = Settings.Ssl.SuppressValidation - ? new TlsHandler(stream => new SslStream(stream, true, (_, _, _, _) => true), new ClientTlsSettings(host)) - : TlsHandler.Client(host, certificate); + TlsHandler tlsHandler; + if (Settings.Ssl.SuppressValidation) + { + // No remote chain validation; do not attach client cert explicitly here + tlsHandler = new TlsHandler(stream => new SslStream(stream, true, (_, _, _, _) => true), new ClientTlsSettings(host)); + } + else if (Settings.Ssl.SendClientCertificate && certificate != null) + { + // Present client certificate + tlsHandler = TlsHandler.Client(host, certificate); + } + else + { + // No client certificate presented + tlsHandler = TlsHandler.Client(host); + } channel.Pipeline.AddFirst("TlsHandler", tlsHandler); } @@ -368,7 +418,21 @@ private void SetServerPipeline(IChannel channel) { if (Settings.EnableSsl) { - channel.Pipeline.AddFirst("TlsHandler", TlsHandler.Server(Settings.Ssl.Certificate)); + if (Settings.Ssl.RequireClientCertificate) + { + // Server requires client certificate + var validator = Settings.Ssl.SuppressValidation + ? new RemoteCertificateValidationCallback((_, _, _, _) => true) + : new RemoteCertificateValidationCallback((_, cert, chain, errors) => errors == SslPolicyErrors.None); + + var serverSettings = new ServerTlsSettings(Settings.Ssl.Certificate, true); + var tlsHandler = new TlsHandler(stream => new SslStream(stream, true, validator), serverSettings); + channel.Pipeline.AddFirst("TlsHandler", tlsHandler); + } + else + { + channel.Pipeline.AddFirst("TlsHandler", TlsHandler.Server(Settings.Ssl.Certificate)); + } } SetInitialChannelPipeline(channel); diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index 26135f86c0d..0b44c07eaa5 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -269,6 +269,10 @@ private static SslSettings Create(Config config) if (config.IsNullOrEmpty()) throw new ConfigurationException($"Failed to create {typeof(DotNettyTransportSettings)}: DotNetty SSL HOCON config was not found (default path: `akka.remote.dot-netty.tcp.ssl`)"); + var failFast = config.GetBoolean("fail-fast-invalid-server-certificate", false); + var requireClientCert = config.GetBoolean("require-client-certificate", false); + var sendClientCert = config.GetBoolean("send-client-certificate", true); + if (config.GetBoolean("certificate.use-thumprint-over-file") || config.GetBoolean("certificate.use-thumbprint-over-file")) { @@ -280,7 +284,10 @@ private static SslSettings Create(Config config) return new SslSettings(certificateThumbprint: thumbprint, storeName: config.GetString("certificate.store-name"), storeLocation: ParseStoreLocationName(config.GetString("certificate.store-location")), - suppressValidation: config.GetBoolean("suppress-validation")); + suppressValidation: config.GetBoolean("suppress-validation"), + failFastInvalidServerCertificate: failFast, + requireClientCertificate: requireClientCert, + sendClientCertificate: sendClientCert); } var flagsRaw = config.GetStringList("certificate.flags", new string[] { }); @@ -290,7 +297,10 @@ private static SslSettings Create(Config config) certificatePath: config.GetString("certificate.path"), certificatePassword: config.GetString("certificate.password"), flags: flags, - suppressValidation: config.GetBoolean("suppress-validation")); + suppressValidation: config.GetBoolean("suppress-validation"), + failFastInvalidServerCertificate: failFast, + requireClientCertificate: requireClientCert, + sendClientCertificate: sendClientCert); } @@ -324,25 +334,39 @@ private static X509KeyStorageFlags ParseKeyStorageFlag(string str) /// X509 certificate used to establish Secure Socket Layer (SSL) between two remote endpoints. /// public readonly X509Certificate2? Certificate; - + /// /// Flag used to suppress certificate validation - use true only, when on dev machine or for testing. /// public readonly bool SuppressValidation; + public readonly bool RequireClientCertificate; + public readonly bool SendClientCertificate; + + /// + /// When enabled, the transport will fail fast at startup if the configured server certificate is not usable + /// for server-side TLS (e.g. missing private key or invalid EKU). + /// + public readonly bool FailFastInvalidServerCertificate; private SslSettings() { Certificate = null; SuppressValidation = false; + RequireClientCertificate = false; + SendClientCertificate = true; + FailFastInvalidServerCertificate = false; } - public SslSettings(X509Certificate2 certificate, bool suppressValidation) + public SslSettings(X509Certificate2 certificate, bool suppressValidation, bool failFastInvalidServerCertificate = false, bool requireClientCertificate = false, bool sendClientCertificate = true) { Certificate = certificate; SuppressValidation = suppressValidation; + FailFastInvalidServerCertificate = failFastInvalidServerCertificate; + RequireClientCertificate = requireClientCertificate; + SendClientCertificate = sendClientCertificate; } - private SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation) + private SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation, bool failFastInvalidServerCertificate, bool requireClientCertificate, bool sendClientCertificate) { using var store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadOnly); @@ -356,15 +380,21 @@ private SslSettings(string certificateThumbprint, string storeName, StoreLocatio Certificate = find[0]; SuppressValidation = suppressValidation; + FailFastInvalidServerCertificate = failFastInvalidServerCertificate; + RequireClientCertificate = requireClientCertificate; + SendClientCertificate = sendClientCertificate; } - private SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation) + private SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation, bool failFastInvalidServerCertificate, bool requireClientCertificate, bool sendClientCertificate) { if (string.IsNullOrEmpty(certificatePath)) throw new ArgumentNullException(nameof(certificatePath), "Path to SSL certificate was not found (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.path`)"); Certificate = new X509Certificate2(certificatePath, certificatePassword, flags); SuppressValidation = suppressValidation; + FailFastInvalidServerCertificate = failFastInvalidServerCertificate; + RequireClientCertificate = requireClientCertificate; + SendClientCertificate = sendClientCertificate; } } } diff --git a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs index 6500dbac076..219d8ef7bad 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs @@ -21,6 +21,20 @@ namespace Akka.Remote.Transport.DotNetty { + internal sealed class TlsHandshakeFailureReason : CoordinatedShutdown.Reason + { + public TlsHandshakeFailureReason(string message) + { + Message = message; + } + + public string Message { get; } + + public override int ExitCode => 79; + + public override string ToString() => Message; + } + internal abstract class TcpHandlers : CommonHandlers { private IHandleEventListener _listener; @@ -72,9 +86,9 @@ public override void UserEventTriggered(IChannelHandlerContext context, object e Log.Error(ex, "TLS handshake failed. Channel [{0}->{1}](Id={2})", context.Channel.LocalAddress, context.Channel.RemoteAddress, context.Channel.Id); - // Best-effort surface to higher layers if listener already registered - NotifyListener(new UnderlyingTransportError(ex, - $"TLS handshake failed on channel [{context.Channel.LocalAddress}->{context.Channel.RemoteAddress}](Id={context.Channel.Id})")); + // Shutdown the ActorSystem on TLS handshake failure + var cs = CoordinatedShutdown.Get(Transport.System); + cs.Run(new TlsHandshakeFailureReason($"TLS handshake failed on channel [{context.Channel.LocalAddress}->{context.Channel.RemoteAddress}](Id={context.Channel.Id})")); context.CloseAsync(); return; // don't pass to next handlers diff --git a/src/core/Akka.Remote/Transport/Transport.cs b/src/core/Akka.Remote/Transport/Transport.cs index bf3f209fd7c..30e13e978f0 100644 --- a/src/core/Akka.Remote/Transport/Transport.cs +++ b/src/core/Akka.Remote/Transport/Transport.cs @@ -203,7 +203,8 @@ public enum DisassociateInfo /// /// TBD /// - Quarantined = 2 + Quarantined = 2, + TlsHandshakeError = 3, } /// From 9bb2bb05ee5d93b7e30df26a5471ff6bb0505e6d Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 25 Sep 2025 01:00:02 +0700 Subject: [PATCH 2/5] Simplify PR --- .../DotNettyTlsHandshakeFailureSpec.cs | 106 +----------------- .../Akka.Remote/Configuration/Remote.conf | 10 -- .../Transport/DotNetty/DotNettyTransport.cs | 75 +------------ .../DotNetty/DotNettyTransportSettings.cs | 40 +------ 4 files changed, 13 insertions(+), 218 deletions(-) diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs index 1a7efb3009d..d453db88c6a 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs @@ -28,7 +28,7 @@ public DotNettyTlsHandshakeFailureSpec(ITestOutputHelper output) : base(Configur { } - private static Config CreateConfig(bool enableSsl, string certPath, string certPassword, bool suppressValidation = true, bool requireClientCert = false, bool sendClientCert = true) + private static Config CreateConfig(bool enableSsl, string certPath, string certPassword, bool suppressValidation = true) { var baseConfig = ConfigurationFactory.ParseString(@"akka { loglevel = DEBUG @@ -48,8 +48,6 @@ private static Config CreateConfig(bool enableSsl, string certPath, string certP var escapedPath = certPath.Replace("\\", "\\\\"); var ssl = $@"akka.remote.dot-netty.tcp.ssl {{ suppress-validation = {(suppressValidation ? "on" : "off")} - require-client-certificate = {(requireClientCert ? "on" : "off")} - send-client-certificate = {(sendClientCert ? "on" : "off")} certificate {{ path = ""{escapedPath}"" password = ""{certPassword ?? string.Empty}"" @@ -68,31 +66,7 @@ private static void CreateCertificateWithoutPrivateKey() File.WriteAllBytes(NoKeyCertPath, publicKeyBytes); } - [Fact] - public void Server_should_fail_fast_when_server_certificate_has_no_private_key() - { - CreateCertificateWithoutPrivateKey(); - - try - { - var baseCfg = CreateConfig(true, NoKeyCertPath, null, suppressValidation: true); - var failfast = ConfigurationFactory.ParseString(@"akka.remote.dot-netty.tcp.ssl.fail-fast-invalid-server-certificate = on"); - var serverConfig = baseCfg.WithFallback(failfast); - - Assert.ThrowsAny(() => - { - using var _ = ActorSystem.Create("ServerSystem", serverConfig); - }); - } - finally - { - try - { - if (File.Exists(NoKeyCertPath)) File.Delete(NoKeyCertPath); - } - catch { /* ignore */ } - } - } + [Fact] public async Task Tls_handshake_failure_should_be_logged_and_shutdown_server() @@ -261,81 +235,7 @@ await AwaitAssertAsync(async () => } } - [Fact] - public async Task MutualTLS_should_succeed_when_client_certificate_is_required_and_provided() - { - ActorSystem server = null; - ActorSystem client = null; - - try - { - // Server requires client certificate but suppresses validation (self-signed ok) - var serverConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: true); - server = ActorSystem.Create("ServerSystem", serverConfig); - InitializeLogger(server, "[SERVER] "); - - // Client sends client certificate - var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: false, sendClientCert: true); - client = ActorSystem.Create("ClientSystem", clientConfig); - InitializeLogger(client, "[CLIENT] "); - - var echo = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); - var serverAddr = RARP.For(server).Provider.DefaultAddress; - var echoPath = new RootActorPath(serverAddr) / "user" / "echo"; - - var probe = CreateTestProbe(client); - await AwaitAssertAsync(async () => - { - client.ActorSelection(echoPath).Tell("mtls-ok", probe.Ref); - await probe.ExpectMsgAsync("mtls-ok", TimeSpan.FromSeconds(1)); - }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200)); - } - finally - { - if (client != null) Shutdown(client, TimeSpan.FromSeconds(10)); - if (server != null) Shutdown(server, TimeSpan.FromSeconds(10)); - } - } - - [Fact] - public async Task MutualTLS_should_shutdown_when_client_certificate_is_required_but_not_provided() - { - ActorSystem server = null; - ActorSystem client = null; - - try - { - // Server requires client certificate, suppress validation for self-signed - var serverConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: false, requireClientCert: true); - server = ActorSystem.Create("ServerSystem", serverConfig); - InitializeLogger(server, "[SERVER] "); - - // Client does NOT send client certificate - var clientConfig = CreateConfig(true, ValidCertPath, Password, suppressValidation: true, requireClientCert: false, sendClientCert: false); - client = ActorSystem.Create("ClientSystem", clientConfig); - InitializeLogger(client, "[CLIENT] "); - - // Create echo on server - var echo = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); - - var serverAddr = RARP.For(server).Provider.DefaultAddress; - var echoPath = new RootActorPath(serverAddr) / "user" / "echo"; - - // Attempt communication; server should shutdown due to TLS failure (client cert required but not provided) - client.ActorSelection(echoPath).Tell("should-fail"); - - await AwaitAssertAsync(async () => - { - Assert.True(server.WhenTerminated.IsCompleted); - await Task.CompletedTask; - }, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200)); - } - finally - { - if (client != null) Shutdown(client, TimeSpan.FromSeconds(10)); - if (server != null) Shutdown(server, TimeSpan.FromSeconds(10)); - } - } + private sealed class EchoActor : ReceiveActor { diff --git a/src/core/Akka.Remote/Configuration/Remote.conf b/src/core/Akka.Remote/Configuration/Remote.conf index 7f5ad1eecd8..9ac9c4cb5af 100644 --- a/src/core/Akka.Remote/Configuration/Remote.conf +++ b/src/core/Akka.Remote/Configuration/Remote.conf @@ -523,16 +523,6 @@ akka { } ssl { - # When enabled, server will require client certificate during TLS handshake (mutual TLS) - require-client-certificate = off - - # When enabled, client will present its certificate during TLS handshake (if available) - send-client-certificate = on - - # When enabled, remoting will fail fast at startup if configured server certificate is unusable - # (e.g. missing private key or lacking Server Authentication EKU). - fail-fast-invalid-server-certificate = off - certificate { # Valid certificate path required path = "" diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index d24bd83a8f5..da828291bcb 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -149,44 +149,8 @@ protected DotNettyTransport(ActorSystem system, Config config) AssociationListenerPromise = new TaskCompletionSource(); SchemeIdentifier = (Settings.EnableSsl ? "ssl." : string.Empty) + Settings.TransportMode.ToString().ToLowerInvariant(); - - if (Settings.EnableSsl && Settings.Ssl.Certificate != null && Settings.Ssl.FailFastInvalidServerCertificate) - { - ValidateServerCertificate(Settings.Ssl.Certificate); - } - } - - private static void ValidateServerCertificate(X509Certificate2 certificate) - { - if (!certificate.HasPrivateKey) - throw new ConfigurationException("Configured SSL certificate is missing a private key and cannot be used for server authentication."); - - try - { - var eku = certificate.Extensions.OfType().FirstOrDefault(); - if (eku != null) - { - var hasServerAuth = eku.EnhancedKeyUsages - .Cast() - .Any(o => string.Equals(o.Value, "1.3.6.1.5.5.7.3.1", StringComparison.Ordinal)); - if (!hasServerAuth) - throw new ConfigurationException("Configured SSL certificate lacks 'Server Authentication' EKU (1.3.6.1.5.5.7.3.1)."); - } - - // Ensure we can access a private key handle - using var _ = certificate.GetRSAPrivateKey() as IDisposable ?? certificate.GetECDsaPrivateKey() as IDisposable; - if (_ == null) - throw new ConfigurationException("Configured SSL certificate does not expose an accessible RSA/ECDSA private key."); - } - catch (ConfigurationException) - { - throw; - } - catch (Exception ex) - { - throw new ConfigurationException("Configured SSL certificate validation failed.", ex); - } } + public DotNettyTransportSettings Settings { get; } public sealed override string SchemeIdentifier { get; protected set; } @@ -383,24 +347,9 @@ private void SetClientPipeline(IChannel channel, Address remoteAddress) { var certificate = Settings.Ssl.Certificate; var host = certificate.GetNameInfo(X509NameType.DnsName, false); - - TlsHandler tlsHandler; - if (Settings.Ssl.SuppressValidation) - { - // No remote chain validation; do not attach client cert explicitly here - tlsHandler = new TlsHandler(stream => new SslStream(stream, true, (_, _, _, _) => true), new ClientTlsSettings(host)); - } - else if (Settings.Ssl.SendClientCertificate && certificate != null) - { - // Present client certificate - tlsHandler = TlsHandler.Client(host, certificate); - } - else - { - // No client certificate presented - tlsHandler = TlsHandler.Client(host); - } - + var tlsHandler = Settings.Ssl.SuppressValidation + ? new TlsHandler(stream => new SslStream(stream, true, (_, _, _, _) => true), new ClientTlsSettings(host)) + : TlsHandler.Client(host, certificate); channel.Pipeline.AddFirst("TlsHandler", tlsHandler); } @@ -418,21 +367,7 @@ private void SetServerPipeline(IChannel channel) { if (Settings.EnableSsl) { - if (Settings.Ssl.RequireClientCertificate) - { - // Server requires client certificate - var validator = Settings.Ssl.SuppressValidation - ? new RemoteCertificateValidationCallback((_, _, _, _) => true) - : new RemoteCertificateValidationCallback((_, cert, chain, errors) => errors == SslPolicyErrors.None); - - var serverSettings = new ServerTlsSettings(Settings.Ssl.Certificate, true); - var tlsHandler = new TlsHandler(stream => new SslStream(stream, true, validator), serverSettings); - channel.Pipeline.AddFirst("TlsHandler", tlsHandler); - } - else - { - channel.Pipeline.AddFirst("TlsHandler", TlsHandler.Server(Settings.Ssl.Certificate)); - } + channel.Pipeline.AddFirst("TlsHandler", TlsHandler.Server(Settings.Ssl.Certificate)); } SetInitialChannelPipeline(channel); diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index 0b44c07eaa5..ecf8137175e 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -269,10 +269,6 @@ private static SslSettings Create(Config config) if (config.IsNullOrEmpty()) throw new ConfigurationException($"Failed to create {typeof(DotNettyTransportSettings)}: DotNetty SSL HOCON config was not found (default path: `akka.remote.dot-netty.tcp.ssl`)"); - var failFast = config.GetBoolean("fail-fast-invalid-server-certificate", false); - var requireClientCert = config.GetBoolean("require-client-certificate", false); - var sendClientCert = config.GetBoolean("send-client-certificate", true); - if (config.GetBoolean("certificate.use-thumprint-over-file") || config.GetBoolean("certificate.use-thumbprint-over-file")) { @@ -284,10 +280,7 @@ private static SslSettings Create(Config config) return new SslSettings(certificateThumbprint: thumbprint, storeName: config.GetString("certificate.store-name"), storeLocation: ParseStoreLocationName(config.GetString("certificate.store-location")), - suppressValidation: config.GetBoolean("suppress-validation"), - failFastInvalidServerCertificate: failFast, - requireClientCertificate: requireClientCert, - sendClientCertificate: sendClientCert); + suppressValidation: config.GetBoolean("suppress-validation")); } var flagsRaw = config.GetStringList("certificate.flags", new string[] { }); @@ -297,10 +290,7 @@ private static SslSettings Create(Config config) certificatePath: config.GetString("certificate.path"), certificatePassword: config.GetString("certificate.password"), flags: flags, - suppressValidation: config.GetBoolean("suppress-validation"), - failFastInvalidServerCertificate: failFast, - requireClientCertificate: requireClientCert, - sendClientCertificate: sendClientCert); + suppressValidation: config.GetBoolean("suppress-validation")); } @@ -339,34 +329,20 @@ private static X509KeyStorageFlags ParseKeyStorageFlag(string str) /// Flag used to suppress certificate validation - use true only, when on dev machine or for testing. /// public readonly bool SuppressValidation; - public readonly bool RequireClientCertificate; - public readonly bool SendClientCertificate; - - /// - /// When enabled, the transport will fail fast at startup if the configured server certificate is not usable - /// for server-side TLS (e.g. missing private key or invalid EKU). - /// - public readonly bool FailFastInvalidServerCertificate; private SslSettings() { Certificate = null; SuppressValidation = false; - RequireClientCertificate = false; - SendClientCertificate = true; - FailFastInvalidServerCertificate = false; } - public SslSettings(X509Certificate2 certificate, bool suppressValidation, bool failFastInvalidServerCertificate = false, bool requireClientCertificate = false, bool sendClientCertificate = true) + public SslSettings(X509Certificate2 certificate, bool suppressValidation) { Certificate = certificate; SuppressValidation = suppressValidation; - FailFastInvalidServerCertificate = failFastInvalidServerCertificate; - RequireClientCertificate = requireClientCertificate; - SendClientCertificate = sendClientCertificate; } - private SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation, bool failFastInvalidServerCertificate, bool requireClientCertificate, bool sendClientCertificate) + private SslSettings(string certificateThumbprint, string storeName, StoreLocation storeLocation, bool suppressValidation) { using var store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadOnly); @@ -380,21 +356,15 @@ private SslSettings(string certificateThumbprint, string storeName, StoreLocatio Certificate = find[0]; SuppressValidation = suppressValidation; - FailFastInvalidServerCertificate = failFastInvalidServerCertificate; - RequireClientCertificate = requireClientCertificate; - SendClientCertificate = sendClientCertificate; } - private SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation, bool failFastInvalidServerCertificate, bool requireClientCertificate, bool sendClientCertificate) + private SslSettings(string certificatePath, string certificatePassword, X509KeyStorageFlags flags, bool suppressValidation) { if (string.IsNullOrEmpty(certificatePath)) throw new ArgumentNullException(nameof(certificatePath), "Path to SSL certificate was not found (by default it can be found under `akka.remote.dot-netty.tcp.ssl.certificate.path`)"); Certificate = new X509Certificate2(certificatePath, certificatePassword, flags); SuppressValidation = suppressValidation; - FailFastInvalidServerCertificate = failFastInvalidServerCertificate; - RequireClientCertificate = requireClientCertificate; - SendClientCertificate = sendClientCertificate; } } } From 00ee2cca8530fc990561f15bc01ef37a3282caf1 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 25 Sep 2025 01:30:20 +0700 Subject: [PATCH 3/5] Simplify PR, remove new DisassociateInfo --- .../verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt | 1 - .../verify/CoreAPISpec.ApproveRemote.Net.verified.txt | 1 - src/core/Akka.Remote/Endpoint.cs | 2 -- src/core/Akka.Remote/Transport/Transport.cs | 1 - 4 files changed, 5 deletions(-) diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt index 622d04dd8ac..bcc6ce10a27 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt @@ -598,7 +598,6 @@ namespace Akka.Remote.Transport Unknown = 0, Shutdown = 1, Quarantined = 2, - TlsHandshakeError = 3, } public sealed class DisassociateUnderlying : Akka.Remote.Transport.TransportOperation, Akka.Event.IDeadLetterSuppression { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt index 7bcf63e17ae..5ae71f05f74 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt @@ -598,7 +598,6 @@ namespace Akka.Remote.Transport Unknown = 0, Shutdown = 1, Quarantined = 2, - TlsHandshakeError = 3, } public sealed class DisassociateUnderlying : Akka.Remote.Transport.TransportOperation, Akka.Event.IDeadLetterSuppression { diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index 02fc5f82293..e4d0117c98b 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -1970,8 +1970,6 @@ private void HandleDisassociated(DisassociateInfo info) "to the remote system are possible until this system is restarted.", LocalAddress, RemoteAddress, disassociateInfo: DisassociateInfo.Quarantined); case DisassociateInfo.Shutdown: throw new ShutDownAssociation($"The remote system terminated the association because it is shutting down. Shut down address: {RemoteAddress}", LocalAddress, RemoteAddress); - case DisassociateInfo.TlsHandshakeError: - throw new TlsHandshakeErrorAssociation("TLS handshake failed.", LocalAddress, RemoteAddress); case DisassociateInfo.Unknown: default: Context.Stop(Self); diff --git a/src/core/Akka.Remote/Transport/Transport.cs b/src/core/Akka.Remote/Transport/Transport.cs index 30e13e978f0..aebd17a3620 100644 --- a/src/core/Akka.Remote/Transport/Transport.cs +++ b/src/core/Akka.Remote/Transport/Transport.cs @@ -204,7 +204,6 @@ public enum DisassociateInfo /// TBD /// Quarantined = 2, - TlsHandshakeError = 3, } /// From 954643708ea42918e71d88f48fbde4df0a5a2026 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 25 Sep 2025 01:42:43 +0700 Subject: [PATCH 4/5] Clean whitespace noise --- .../Transport/DotNettyTlsHandshakeFailureSpec.cs | 1 - src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs | 3 ++- .../Transport/DotNetty/DotNettyTransportSettings.cs | 2 +- src/core/Akka.Remote/Transport/Transport.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs index d453db88c6a..c2831ca0432 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs @@ -33,7 +33,6 @@ private static Config CreateConfig(bool enableSsl, string certPath, string certP var baseConfig = ConfigurationFactory.ParseString(@"akka { loglevel = DEBUG actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" - remote.retry-gate-closed-for = 3s remote.dot-netty.tcp { port = 0 hostname = ""127.0.0.1"" diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index da828291bcb..c62da27b042 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -150,7 +150,6 @@ protected DotNettyTransport(ActorSystem system, Config config) SchemeIdentifier = (Settings.EnableSsl ? "ssl." : string.Empty) + Settings.TransportMode.ToString().ToLowerInvariant(); } - public DotNettyTransportSettings Settings { get; } public sealed override string SchemeIdentifier { get; protected set; } @@ -347,9 +346,11 @@ private void SetClientPipeline(IChannel channel, Address remoteAddress) { var certificate = Settings.Ssl.Certificate; var host = certificate.GetNameInfo(X509NameType.DnsName, false); + var tlsHandler = Settings.Ssl.SuppressValidation ? new TlsHandler(stream => new SslStream(stream, true, (_, _, _, _) => true), new ClientTlsSettings(host)) : TlsHandler.Client(host, certificate); + channel.Pipeline.AddFirst("TlsHandler", tlsHandler); } diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index ecf8137175e..26135f86c0d 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -324,7 +324,7 @@ private static X509KeyStorageFlags ParseKeyStorageFlag(string str) /// X509 certificate used to establish Secure Socket Layer (SSL) between two remote endpoints. /// public readonly X509Certificate2? Certificate; - + /// /// Flag used to suppress certificate validation - use true only, when on dev machine or for testing. /// diff --git a/src/core/Akka.Remote/Transport/Transport.cs b/src/core/Akka.Remote/Transport/Transport.cs index aebd17a3620..bf3f209fd7c 100644 --- a/src/core/Akka.Remote/Transport/Transport.cs +++ b/src/core/Akka.Remote/Transport/Transport.cs @@ -203,7 +203,7 @@ public enum DisassociateInfo /// /// TBD /// - Quarantined = 2, + Quarantined = 2 } /// From 3a0c56215f7a686d789b2e3f01dd79c3a87d0f4a Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 25 Sep 2025 01:51:16 +0700 Subject: [PATCH 5/5] cleanup, remove TlsHandshakeErrorAssociation --- .../DotNettyTlsHandshakeFailureSpec.cs | 4 +-- src/core/Akka.Remote/Endpoint.cs | 28 ------------------- src/core/Akka.Remote/EndpointManager.cs | 4 --- 3 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs index c2831ca0432..9332445cf63 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs @@ -65,7 +65,7 @@ private static void CreateCertificateWithoutPrivateKey() File.WriteAllBytes(NoKeyCertPath, publicKeyBytes); } - + [Fact] public async Task Tls_handshake_failure_should_be_logged_and_shutdown_server() @@ -234,7 +234,7 @@ await AwaitAssertAsync(async () => } } - + private sealed class EchoActor : ReceiveActor { diff --git a/src/core/Akka.Remote/Endpoint.cs b/src/core/Akka.Remote/Endpoint.cs index e4d0117c98b..3c6168ac2d0 100644 --- a/src/core/Akka.Remote/Endpoint.cs +++ b/src/core/Akka.Remote/Endpoint.cs @@ -209,35 +209,7 @@ protected EndpointException(SerializationInfo info, StreamingContext context) /// internal interface IAssociationProblem { } - /// - /// INTERNAL API - /// - internal sealed class TlsHandshakeErrorAssociation : EndpointException, IAssociationProblem - { - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - /// TBD - public TlsHandshakeErrorAssociation(string message, Address localAddress, Address remoteAddress, Exception cause = null) - : base(message, cause) - { - RemoteAddress = remoteAddress; - LocalAddress = localAddress; - } - /// - /// TBD - /// - public Address LocalAddress { get; private set; } - - /// - /// TBD - /// - public Address RemoteAddress { get; private set; } - } /// /// INTERNAL API diff --git a/src/core/Akka.Remote/EndpointManager.cs b/src/core/Akka.Remote/EndpointManager.cs index 802cecd8893..3f1c1fce344 100644 --- a/src/core/Akka.Remote/EndpointManager.cs +++ b/src/core/Akka.Remote/EndpointManager.cs @@ -570,10 +570,6 @@ Directive Hopeless(HopelessAssociation e) switch (ex) { - case TlsHandshakeErrorAssociation tls: - _log.Error("Shutting down ActorSystem due to TLS handshake failure between [{0}] and [{1}]", tls.LocalAddress, tls.RemoteAddress); - CoordinatedShutdown.Get(Context.System).Run(new TlsHandshakeFailureReason($"TLS handshake failure between [{tls.LocalAddress}] and [{tls.RemoteAddress}]")); - return Directive.Stop; case InvalidAssociation ia: KeepQuarantinedOr(ia.RemoteAddress, () => {