-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix TLS hostname validation bug and add configurable validation #7897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
abcf971
96d289a
79987e3
e980535
23c25f9
5dbd91e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
| "Hasher", | ||
| "Hipsterize", | ||
| "HOCON", | ||
| "hostnames", | ||
| "journaled", | ||
| "Kubernetes", | ||
| "lifecycles", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,7 @@ public DotNettyMutualTlsSpec(ITestOutputHelper output) : base(ConfigurationFacto | |
| { | ||
| } | ||
|
|
||
| private static Config CreateConfig(bool enableSsl, bool requireMutualAuth, bool suppressValidation = false, string certPath = null) | ||
| private static Config CreateConfig(bool enableSsl, bool requireMutualAuth, bool suppressValidation = false, string certPath = null, bool? validateCertificateHostname = null) | ||
| { | ||
| var config = ConfigurationFactory.ParseString($@" | ||
| akka {{ | ||
|
|
@@ -49,10 +49,15 @@ private static Config CreateConfig(bool enableSsl, bool requireMutualAuth, bool | |
| return config; | ||
|
|
||
| var escapedPath = (certPath ?? ValidCertPath).Replace("\\", "\\\\"); | ||
| var hostnameValidationConfig = validateCertificateHostname.HasValue | ||
| ? $"validate-certificate-hostname = {(validateCertificateHostname.Value ? "on" : "off")}" | ||
| : ""; | ||
|
|
||
| var ssl = $@" | ||
| akka.remote.dot-netty.tcp.ssl {{ | ||
| suppress-validation = {(suppressValidation ? "on" : "off")} | ||
| require-mutual-authentication = {(requireMutualAuth ? "on" : "off")} | ||
| {hostnameValidationConfig} | ||
| certificate {{ | ||
| path = ""{escapedPath}"" | ||
| password = ""{Password}"" | ||
|
|
@@ -275,6 +280,165 @@ await Assert.ThrowsAsync<AskTimeoutException>(async () => | |
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "Different certificates with hostname validation disabled should connect successfully")] | ||
| public async Task Hostname_validation_disabled_should_allow_different_certificates() | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a tad difficult for us to robustly test this functionality without doing things like installing custom CAs, so we're doing this all with self-signed certificates with hostname validation and CA validation both disabled. |
||
| { | ||
| // Per-node certificates should work when hostname validation is disabled | ||
| // Note: Using suppressValidation=true to bypass chain validation since test certs are self-signed | ||
| // This isolates the hostname validation logic we're testing | ||
| ActorSystem server = null; | ||
| ActorSystem client = null; | ||
|
|
||
| try | ||
| { | ||
| // Server with one certificate, hostname validation disabled | ||
| var serverConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ValidCertPath, validateCertificateHostname: false); | ||
| server = ActorSystem.Create("ServerSystem", serverConfig); | ||
| InitializeLogger(server, "[SERVER] "); | ||
|
|
||
| var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); | ||
| var serverAddr = RARP.For(server).Provider.DefaultAddress; | ||
| var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; | ||
|
|
||
| // Client with different certificate, hostname validation disabled | ||
| var clientConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ClientCertPath, validateCertificateHostname: false); | ||
| client = ActorSystem.Create("ClientSystem", clientConfig); | ||
| InitializeLogger(client, "[CLIENT] "); | ||
|
|
||
| // Should successfully connect because hostname validation is disabled | ||
| var response = await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(5)); | ||
| Assert.Equal("hello", response); | ||
| } | ||
| finally | ||
| { | ||
| if (client != null) | ||
| Shutdown(client, TimeSpan.FromSeconds(10)); | ||
| if (server != null) | ||
| Shutdown(server, TimeSpan.FromSeconds(10)); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "Different certificates with hostname validation enabled should fail with name mismatch")] | ||
| public async Task Hostname_validation_enabled_should_reject_different_certificates() | ||
| { | ||
| // When hostname validation is enabled, different certificates should fail with RemoteCertificateNameMismatch | ||
| // Note: Using suppressValidation=true to bypass chain validation and test hostname validation specifically | ||
| ActorSystem server = null; | ||
| ActorSystem client = null; | ||
|
|
||
| try | ||
| { | ||
| // Server with one certificate, hostname validation enabled | ||
| var serverConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ValidCertPath, validateCertificateHostname: true); | ||
| server = ActorSystem.Create("ServerSystem", serverConfig); | ||
| InitializeLogger(server, "[SERVER] "); | ||
|
|
||
| var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); | ||
| var serverAddr = RARP.For(server).Provider.DefaultAddress; | ||
| var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; | ||
|
|
||
| // Client with different certificate, hostname validation enabled | ||
| var clientConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ClientCertPath, validateCertificateHostname: true); | ||
| client = ActorSystem.Create("ClientSystem", clientConfig); | ||
| InitializeLogger(client, "[CLIENT] "); | ||
|
|
||
| // Should fail because hostname in certificate doesn't match connection target (127.0.0.1) | ||
| await Assert.ThrowsAsync<AskTimeoutException>(async () => | ||
| { | ||
| await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(3)); | ||
| }); | ||
| } | ||
| finally | ||
| { | ||
| if (client != null) | ||
| Shutdown(client, TimeSpan.FromSeconds(10)); | ||
| if (server != null) | ||
| Shutdown(server, TimeSpan.FromSeconds(10)); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "Same certificate should connect successfully (typical mutual TLS scenario)")] | ||
| public async Task Same_certificate_should_connect_in_mutual_tls() | ||
| { | ||
| // Typical mutual TLS: Both nodes use the same shared certificate | ||
| // Hostname validation disabled because we're using IPs/per-node certs | ||
| ActorSystem server = null; | ||
| ActorSystem client = null; | ||
|
|
||
| try | ||
| { | ||
| // Server with same certificate, hostname validation disabled (typical for mutual TLS) | ||
| var serverConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ValidCertPath, validateCertificateHostname: false); | ||
| server = ActorSystem.Create("ServerSystem", serverConfig); | ||
| InitializeLogger(server, "[SERVER] "); | ||
|
|
||
| var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); | ||
| var serverAddr = RARP.For(server).Provider.DefaultAddress; | ||
| var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; | ||
|
|
||
| // Client with same certificate, hostname validation disabled | ||
| var clientConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ValidCertPath, validateCertificateHostname: false); | ||
| client = ActorSystem.Create("ClientSystem", clientConfig); | ||
| InitializeLogger(client, "[CLIENT] "); | ||
|
|
||
| // Should successfully connect - typical mutual TLS scenario | ||
| var response = await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(5)); | ||
| Assert.Equal("hello", response); | ||
| } | ||
| finally | ||
| { | ||
| if (client != null) | ||
| Shutdown(client, TimeSpan.FromSeconds(10)); | ||
| if (server != null) | ||
| Shutdown(server, TimeSpan.FromSeconds(10)); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "Hostname validation unspecified should default to disabled (backward compatibility)")] | ||
| public async Task Hostname_validation_default_should_be_disabled() | ||
| { | ||
| // When validate-certificate-hostname is not specified, it should default to false | ||
| // Note: Using suppressValidation=true to bypass chain validation and test hostname default behavior | ||
| ActorSystem server = null; | ||
| ActorSystem client = null; | ||
|
|
||
| try | ||
| { | ||
| // Server without specifying hostname validation (should default to false) | ||
| var serverConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ValidCertPath, validateCertificateHostname: null); | ||
| server = ActorSystem.Create("ServerSystem", serverConfig); | ||
| InitializeLogger(server, "[SERVER] "); | ||
|
|
||
| var serverEcho = server.ActorOf(Props.Create(() => new EchoActor()), "echo"); | ||
| var serverAddr = RARP.For(server).Provider.DefaultAddress; | ||
| var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo"; | ||
|
|
||
| // Client with different certificate, hostname validation unspecified (should default to false) | ||
| var clientConfig = CreateConfig(enableSsl: true, requireMutualAuth: true, suppressValidation: true, | ||
| certPath: ClientCertPath, validateCertificateHostname: null); | ||
| client = ActorSystem.Create("ClientSystem", clientConfig); | ||
| InitializeLogger(client, "[CLIENT] "); | ||
|
|
||
| // Should successfully connect because hostname validation defaults to disabled | ||
| var response = await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(5)); | ||
| Assert.Equal("hello", response); | ||
| } | ||
| finally | ||
| { | ||
| if (client != null) | ||
| Shutdown(client, TimeSpan.FromSeconds(10)); | ||
| if (server != null) | ||
| Shutdown(server, TimeSpan.FromSeconds(10)); | ||
| } | ||
| } | ||
|
|
||
| private sealed class EchoActor : ReceiveActor | ||
| { | ||
| public EchoActor() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,96 @@ await Assert.ThrowsAsync<RemoteTransportException>(async () => | |
| }); | ||
| } | ||
|
|
||
| [Fact(DisplayName = "DotNettySslSetup with 2 parameters should configure effective DotNettyTransportSettings with defaults (RequireMutualAuth=true, ValidateHostname=false)")] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the |
||
| public void Two_parameter_setup_should_configure_transport_settings_with_defaults() | ||
| { | ||
| var certificate = new X509Certificate2(ValidCertPath, Password, X509KeyStorageFlags.DefaultKeySet); | ||
| var sslSetup = new DotNettySslSetup(certificate, suppressValidation: true); | ||
|
|
||
| var actorSystemSetup = ActorSystemSetup.Empty | ||
| .And(BootstrapSetup.Create().WithConfig(ConfigurationFactory.ParseString(@" | ||
| akka { | ||
| actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" | ||
| remote.dot-netty.tcp { | ||
| port = 0 | ||
| hostname = ""127.0.0.1"" | ||
| enable-ssl = true | ||
| } | ||
| }"))) | ||
| .And(sslSetup); | ||
|
|
||
| using var sys = ActorSystem.Create("test", actorSystemSetup); | ||
|
|
||
| // Verify that DotNettyTransportSettings.Create uses the setup correctly | ||
| var settings = DotNettyTransportSettings.Create(sys); | ||
|
|
||
| Assert.True(settings.EnableSsl); | ||
| Assert.Equal(certificate, settings.Ssl.Certificate); | ||
| Assert.True(settings.Ssl.SuppressValidation); | ||
| Assert.True(settings.Ssl.RequireMutualAuthentication); // default from 2-param constructor | ||
| Assert.False(settings.Ssl.ValidateCertificateHostname); // default from 2-param constructor | ||
| } | ||
|
|
||
| [Fact(DisplayName = "DotNettySslSetup with 3 parameters should configure effective DotNettyTransportSettings with specified RequireMutualAuth and default ValidateHostname=false")] | ||
| public void Three_parameter_setup_should_configure_transport_settings() | ||
| { | ||
| var certificate = new X509Certificate2(ValidCertPath, Password, X509KeyStorageFlags.DefaultKeySet); | ||
| var sslSetup = new DotNettySslSetup(certificate, suppressValidation: false, requireMutualAuthentication: false); | ||
|
|
||
| var actorSystemSetup = ActorSystemSetup.Empty | ||
| .And(BootstrapSetup.Create().WithConfig(ConfigurationFactory.ParseString(@" | ||
| akka { | ||
| actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" | ||
| remote.dot-netty.tcp { | ||
| port = 0 | ||
| hostname = ""127.0.0.1"" | ||
| enable-ssl = true | ||
| } | ||
| }"))) | ||
| .And(sslSetup); | ||
|
|
||
| using var sys = ActorSystem.Create("test", actorSystemSetup); | ||
|
|
||
| // Verify that DotNettyTransportSettings.Create uses the setup correctly | ||
| var settings = DotNettyTransportSettings.Create(sys); | ||
|
|
||
| Assert.True(settings.EnableSsl); | ||
| Assert.Equal(certificate, settings.Ssl.Certificate); | ||
| Assert.False(settings.Ssl.SuppressValidation); | ||
| Assert.False(settings.Ssl.RequireMutualAuthentication); // explicitly set to false | ||
| Assert.False(settings.Ssl.ValidateCertificateHostname); // default from 3-param constructor | ||
| } | ||
|
|
||
| [Fact(DisplayName = "DotNettySslSetup with 4 parameters should configure effective DotNettyTransportSettings with all specified values")] | ||
| public void Four_parameter_setup_should_configure_transport_settings_with_all_values() | ||
| { | ||
| var certificate = new X509Certificate2(ValidCertPath, Password, X509KeyStorageFlags.DefaultKeySet); | ||
| var sslSetup = new DotNettySslSetup(certificate, suppressValidation: true, requireMutualAuthentication: false, validateCertificateHostname: true); | ||
|
|
||
| var actorSystemSetup = ActorSystemSetup.Empty | ||
| .And(BootstrapSetup.Create().WithConfig(ConfigurationFactory.ParseString(@" | ||
| akka { | ||
| actor.provider = ""Akka.Remote.RemoteActorRefProvider,Akka.Remote"" | ||
| remote.dot-netty.tcp { | ||
| port = 0 | ||
| hostname = ""127.0.0.1"" | ||
| enable-ssl = true | ||
| } | ||
| }"))) | ||
| .And(sslSetup); | ||
|
|
||
| using var sys = ActorSystem.Create("test", actorSystemSetup); | ||
|
|
||
| // Verify that DotNettyTransportSettings.Create uses the setup correctly | ||
| var settings = DotNettyTransportSettings.Create(sys); | ||
|
|
||
| Assert.True(settings.EnableSsl); | ||
| Assert.Equal(certificate, settings.Ssl.Certificate); | ||
| Assert.True(settings.Ssl.SuppressValidation); | ||
| Assert.False(settings.Ssl.RequireMutualAuthentication); // explicitly set to false | ||
| Assert.True(settings.Ssl.ValidateCertificateHostname); // explicitly set to true | ||
| } | ||
|
|
||
| #region helper classes / methods | ||
|
|
||
| protected override void AfterAll() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -565,6 +565,18 @@ akka { | |
| # Set to false only if your environment cannot support client certificate authentication. | ||
| # Default: true (secure by default) | ||
| require-mutual-authentication = true | ||
|
|
||
| # Enable or disable certificate hostname validation during TLS handshake. | ||
| # When true: Traditional TLS hostname validation is performed (certificate CN/SAN must match target hostname) | ||
| # When false: Only validates certificate chain against CA, ignores hostname mismatches | ||
| # | ||
| # Set to false for scenarios such as: | ||
| # - Mutual TLS with per-node certificates in P2P clusters | ||
| # - IP-based connections where certificates use DNS names | ||
| # - Service discovery with dynamic addresses | ||
| # | ||
| # Default: false (disabled for backward compatibility and mutual TLS flexibility) | ||
| validate-certificate-hostname = false | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New setting, for validating the hostnames on the provided certificates. This should default to false so IP-based connections can still function correctly, but it can be enabled for zero-trust environments. |
||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaned up the docs and mentioned the new settings introduced in this PR, along with where certs are sourced from in the store / how they're used.