Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
<PropertyGroup>
<Copyright>Copyright © 2013-2025 Akka.NET Team</Copyright>
<Authors>Akka.NET Team</Authors>
<VersionPrefix>1.5.52</VersionPrefix>
<PackageReleaseNotes>**API Changes**
<VersionPrefix>1.5.53</VersionPrefix>
<PackageReleaseNotes>**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 =&gt; journal.AddWriteEventAdapter&lt;T&gt;(...))`. 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)</PackageReleaseNotes>
* [Bump Akka version from 1.5.52 to 1.5.53](https://github.com/akkadotnet/akka.net/releases/tag/1.5.53)</PackageReleaseNotes>
<PackageIcon>akkalogo.png</PackageIcon>
<PackageProjectUrl>
https://github.com/akkadotnet/Akka.Hosting
Expand All @@ -29,7 +34,7 @@
<TestSdkVersion>17.11.1</TestSdkVersion>
<CoverletVersion>6.0.3</CoverletVersion>
<XunitRunneVisualstudio>3.1.5</XunitRunneVisualstudio>
<AkkaVersion>1.5.52</AkkaVersion>
<AkkaVersion>1.5.53</AkkaVersion>
<MicrosoftExtensionsVersion>[6.0.0,)</MicrosoftExtensionsVersion>
<SystemTextJsonVersion>[6.0.10,)</SystemTextJsonVersion>
</PropertyGroup>
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
113 changes: 111 additions & 2 deletions src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
65 changes: 57 additions & 8 deletions src/Akka.Remote.Hosting/RemoteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -197,18 +209,55 @@ public sealed class SslOptions
public X509Certificate2? X509Certificate { get; set; }
public SslCertificateOptions CertificateOptions { get; set; } = new ();

/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// This provides defense-in-depth security by ensuring bidirectional authentication and
/// preventing asymmetric connectivity issues in peer-to-peer Akka.Remote connections.
/// </para>
/// <b>Default:</b> true (as of Akka.NET v1.5.52)
/// </summary>
public bool? RequireMutualAuthentication { get; set; }

/// <summary>
/// <para>
/// Controls whether certificate hostname validation is performed during TLS handshake.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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
/// </para>
/// <b>Default:</b> false (as of Akka.NET v1.5.53)
/// </summary>
public bool? ValidateCertificateHostname { get; set; }

internal void Build(StringBuilder builder)
{
var sb = new StringBuilder();

if (SuppressValidation is not null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we supposed to use DotNettySslSetup anywhere in this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do, in line 118

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);
Expand Down