diff --git a/Directory.Build.props b/Directory.Build.props index 1e0c7fbb..2f0a8d92 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,12 +2,17 @@ Copyright © 2013-2025 Akka.NET Team Akka.NET Team - 1.5.52 - **API Changes** + 1.5.53 + **New Features** +* [Add SSL/TLS configuration options from Akka.NET v1.5.52 and v1.5.53](https://github.com/akkadotnet/Akka.Hosting/pull/XXX) - Added support for new SSL/TLS configuration options: + * `RequireMutualAuthentication` - Enables mutual TLS (mTLS) authentication (default: true) + * `ValidateCertificateHostname` - Controls certificate hostname validation (default: false) + +**API Changes** * [Deprecate JournalOptions.Adapters property in favor of callback API](https://github.com/akkadotnet/Akka.Hosting/pull/669) - resolved [issue #665](https://github.com/akkadotnet/Akka.Hosting/issues/665) by deprecating the `JournalOptions.Adapters` property. Users should migrate to the unified callback pattern: `builder.WithJournal(options, journal => journal.AddWriteEventAdapter<T>(...))`. The deprecated property will be removed in v1.6.0. **Updates** -* [Bump Akka version from 1.5.51 to 1.5.52](https://github.com/akkadotnet/akka.net/releases/tag/1.5.52) +* [Bump Akka version from 1.5.52 to 1.5.53](https://github.com/akkadotnet/akka.net/releases/tag/1.5.53) akkalogo.png https://github.com/akkadotnet/Akka.Hosting @@ -29,7 +34,7 @@ 17.11.1 6.0.3 3.1.5 - 1.5.52 + 1.5.53 [6.0.0,) [6.0.10,) diff --git a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt index cd562a4f..ad14c62d 100644 --- a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt +++ b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt @@ -54,7 +54,9 @@ { public SslOptions() { } public Akka.Remote.Hosting.SslCertificateOptions CertificateOptions { get; set; } + public bool? RequireMutualAuthentication { get; set; } public bool? SuppressValidation { get; set; } + public bool? ValidateCertificateHostname { get; set; } public System.Security.Cryptography.X509Certificates.X509Certificate2? X509Certificate { get; set; } } } \ No newline at end of file diff --git a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs index ad434e79..995b18e7 100644 --- a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs +++ b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs @@ -433,17 +433,126 @@ public void WithRemotingOptionsSslDisabledCertificateTest() EnableSsl = false, Ssl = new SslOptions { - SuppressValidation = true, + SuppressValidation = true, X509Certificate = certificate } }); - + // act var setup = builder.Setups.FirstOrDefault(s => s is DotNettySslSetup); // assert setup.Should().BeNull(); } + + [Fact(DisplayName = "RemoteOptions with new SSL/TLS settings should generate correct HOCON configuration")] + public void WithRemotingNewSslSettingsHoconTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + EnableSsl = true, + Ssl = new SslOptions + { + SuppressValidation = false, + RequireMutualAuthentication = false, // Explicitly set to false for testing + ValidateCertificateHostname = true, // Explicitly set to true for testing + X509Certificate = certificate + } + }); + + // act + var config = builder.Configuration.Value; + var sslConfig = config.GetConfig("akka.remote.dot-netty.tcp.ssl"); + + // assert + sslConfig.GetBoolean("suppress-validation").Should().BeFalse(); + sslConfig.GetBoolean("require-mutual-authentication").Should().BeFalse(); + sslConfig.GetBoolean("validate-certificate-hostname").Should().BeTrue(); + } + + [Fact(DisplayName = "RemoteOptions with new SSL/TLS settings should properly configure DotNettySslSetup")] + public void WithRemotingNewSslSettingsDotNettySslSetupTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + EnableSsl = true, + Ssl = new SslOptions + { + SuppressValidation = false, + RequireMutualAuthentication = false, + ValidateCertificateHostname = true, + X509Certificate = certificate + } + }); + + // act + var setup = (DotNettySslSetup)builder.Setups.First(s => s is DotNettySslSetup); + + // assert + setup.SuppressValidation.Should().BeFalse(); + setup.Certificate.Should().Be(certificate); + // Note: The RequireMutualAuthentication and ValidateCertificateHostname properties + // are now passed to DotNettySslSetup via the 4-parameter constructor in Akka.NET v1.5.53 + setup.RequireMutualAuthentication.Should().BeFalse(); + setup.ValidateCertificateHostname.Should().BeTrue(); + } + + [Fact(DisplayName = "RemoteOptions without new SSL/TLS settings should use default values")] + public void WithRemotingDefaultSslSettingsTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + EnableSsl = true, + Ssl = new SslOptions + { + X509Certificate = certificate + // RequireMutualAuthentication and ValidateCertificateHostname not specified + } + }); + + // act + var setup = (DotNettySslSetup)builder.Setups.First(s => s is DotNettySslSetup); + + // assert + setup.Should().NotBeNull(); + setup.Certificate.Should().Be(certificate); + setup.RequireMutualAuthentication.Should().BeTrue(); + setup.ValidateCertificateHostname.Should().BeFalse(); + } + + [Fact(DisplayName = "RemoteOptions using configurator should set new SSL/TLS properties correctly")] + public void WithRemotingConfiguratorNewSslSettingsTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(opt => + { + opt.EnableSsl = true; + opt.Ssl.RequireMutualAuthentication = true; + opt.Ssl.ValidateCertificateHostname = false; + opt.Ssl.X509Certificate = certificate; + }); + + // act + var config = builder.Configuration.Value; + var tcpConfig = config.GetConfig("akka.remote.dot-netty.tcp"); + var sslConfig = tcpConfig.GetConfig("ssl"); + + // assert + tcpConfig.GetBoolean("enable-ssl").Should().BeTrue(); + sslConfig.GetBoolean("require-mutual-authentication").Should().BeTrue(); + sslConfig.GetBoolean("validate-certificate-hostname").Should().BeFalse(); + } [Fact] public async Task AkkaRemoteShouldUsePublicHostnameCorrectly() diff --git a/src/Akka.Remote.Hosting/RemoteOptions.cs b/src/Akka.Remote.Hosting/RemoteOptions.cs index 7bebad6a..9d93954e 100644 --- a/src/Akka.Remote.Hosting/RemoteOptions.cs +++ b/src/Akka.Remote.Hosting/RemoteOptions.cs @@ -104,12 +104,24 @@ internal void Build(AkkaConfigurationBuilder builder) if (sb.Length > 0) builder.AddHocon(sb.ToString(), HoconAddMode.Prepend); - - if (EnableSsl is false || Ssl.X509Certificate == null) + + if (EnableSsl is false || Ssl.X509Certificate == null) return; - + var suppressValidation = Ssl.SuppressValidation ?? false; - builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation)); + var requireMutualAuth = Ssl.RequireMutualAuthentication ?? true; // Default to true as per v1.5.52 + var validateHostname = Ssl.ValidateCertificateHostname ?? false; // Default to false as per v1.5.53 + + // Use the 4-parameter constructor if any of the new settings are provided, otherwise use the legacy constructor for backward compatibility + if (Ssl.RequireMutualAuthentication.HasValue || Ssl.ValidateCertificateHostname.HasValue) + { + builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation, requireMutualAuth, validateHostname)); + } + else + { + // Use legacy constructor for backward compatibility when new settings are not specified + builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation)); + } } private void Build(StringBuilder builder) @@ -197,18 +209,55 @@ public sealed class SslOptions public X509Certificate2? X509Certificate { get; set; } public SslCertificateOptions CertificateOptions { get; set; } = new (); + /// + /// + /// When set to true, enables mutual TLS (mTLS) authentication where both client and server + /// must present valid certificates with accessible private keys during the TLS handshake. + /// + /// + /// This provides defense-in-depth security by ensuring bidirectional authentication and + /// preventing asymmetric connectivity issues in peer-to-peer Akka.Remote connections. + /// + /// Default: true (as of Akka.NET v1.5.52) + /// + public bool? RequireMutualAuthentication { get; set; } + + /// + /// + /// Controls whether certificate hostname validation is performed during TLS handshake. + /// + /// + /// When enabled (true): Traditional TLS hostname validation is performed - certificate CN/SAN must match the target hostname. + /// When disabled (false): Only validates certificate chain against CA, ignores hostname mismatches. + /// + /// + /// Disabling hostname validation may be necessary for: + /// - Mutual TLS with per-node certificates in P2P clusters + /// - IP-based connections where certificates use DNS names + /// - Service discovery with dynamic addresses + /// + /// Default: false (as of Akka.NET v1.5.53) + /// + public bool? ValidateCertificateHostname { get; set; } + internal void Build(StringBuilder builder) { var sb = new StringBuilder(); - + if (SuppressValidation is not null) sb.AppendLine($"suppress-validation = {SuppressValidation.ToHocon()}"); - + + if (RequireMutualAuthentication is not null) + sb.AppendLine($"require-mutual-authentication = {RequireMutualAuthentication.ToHocon()}"); + + if (ValidateCertificateHostname is not null) + sb.AppendLine($"validate-certificate-hostname = {ValidateCertificateHostname.ToHocon()}"); + CertificateOptions.Build(sb); - + if(sb.Length == 0) return; - + sb.Insert(0, "ssl {"); sb.AppendLine("}"); builder.Append(sb);