From 5dcb4f1ff144d14d13ecb50b002a04d1bea327a9 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 7 Jul 2026 03:35:08 -0400 Subject: [PATCH] feat(security): decouple transport tls from auth Signed-off-by: Yordis Prieto --- .../AuthorizationPolicyRegistryFactory.cs | 2 +- .../ClusterVNodeHostedService.cs | 4 +- .../Services/ConfigurationBrowserService.cs | 9 ++- src/EventStore.ClusterNode/Program.cs | 8 +++ .../when_building/with_default_settings.cs | 1 + .../when_building/with_secure_tcp.cs | 63 +++++++++++++++++++ .../Configuration/ClusterVNodeOptionsTests.cs | 22 +++++++ .../OptionsCertificateProvider.cs | 2 +- src/EventStore.Core/ClusterVNode.cs | 38 +++++------ .../Configuration/ClusterVNodeOptions.cs | 7 +++ .../ClusterVNodeOptionsValidator.cs | 2 +- .../Telemetry/TelemetryService.cs | 1 + 12 files changed, 132 insertions(+), 27 deletions(-) diff --git a/src/EventStore.ClusterNode/AuthorizationPolicyRegistryFactory.cs b/src/EventStore.ClusterNode/AuthorizationPolicyRegistryFactory.cs index 63eb5b1adb..dbd12997fb 100644 --- a/src/EventStore.ClusterNode/AuthorizationPolicyRegistryFactory.cs +++ b/src/EventStore.ClusterNode/AuthorizationPolicyRegistryFactory.cs @@ -27,7 +27,7 @@ public class AuthorizationPolicyRegistryFactory : SubsystemsPlugin public AuthorizationPolicyRegistryFactory(ClusterVNodeOptions options, IConfiguration configuration, PluginLoader pluginLoader) { - if (options.Application.Insecure) + if (options.Application.AuthDisabled()) { _createRegistry = _ => new StaticAuthorizationPolicyRegistry([]); return; diff --git a/src/EventStore.ClusterNode/ClusterVNodeHostedService.cs b/src/EventStore.ClusterNode/ClusterVNodeHostedService.cs index 04835eec16..3ef4f7dc1d 100644 --- a/src/EventStore.ClusterNode/ClusterVNodeHostedService.cs +++ b/src/EventStore.ClusterNode/ClusterVNodeHostedService.cs @@ -134,7 +134,7 @@ public ClusterVNodeHostedService( (ClusterVNodeOptions, AuthorizationProviderFactory) GetAuthorizationProviderFactory() { - if (_options.Application.Insecure) + if (_options.Application.AuthDisabled()) { return (_options, new AuthorizationProviderFactory(_ => new PassthroughAuthorizationProviderFactory())); } @@ -241,7 +241,7 @@ IPersistentSubscriptionConsumerStrategyFactory[] GetPersistentSubscriptionConsum AuthenticationProviderFactory GetAuthenticationProviderFactory() { - if (_options.Application.Insecure) + if (_options.Application.AuthDisabled()) { return new AuthenticationProviderFactory(_ => new PassthroughAuthenticationProviderFactory()); } diff --git a/src/EventStore.ClusterNode/Components/Services/ConfigurationBrowserService.cs b/src/EventStore.ClusterNode/Components/Services/ConfigurationBrowserService.cs index 53872210a8..6760489a0a 100644 --- a/src/EventStore.ClusterNode/Components/Services/ConfigurationBrowserService.cs +++ b/src/EventStore.ClusterNode/Components/Services/ConfigurationBrowserService.cs @@ -21,7 +21,8 @@ public ConfigurationPage Read() var options = hostedService.Options; var features = new[] { new ConfigurationFeature("Projections", options.Projection.RunProjections != ProjectionType.None || options.DevMode.Dev), - new ConfigurationFeature("User management", options.Auth.AuthenticationType == Opts.AuthenticationTypeDefault && !options.Application.Insecure) + new ConfigurationFeature("User management", + options.Auth.AuthenticationType == Opts.AuthenticationTypeDefault && !options.Application.AuthDisabled()) }; var subsystems = hostedService.EnabledNodeSubsystems @@ -46,6 +47,7 @@ public ConfigurationPage Read() canInspectRuntimeDetails ? options.Auth.AuthenticationType : "", canInspectRuntimeDetails ? options.Auth.AuthorizationType : "", options.Application.Insecure, + options.Application.TlsDisabled(), canInspectRuntimeDetails, features, subsystems, @@ -66,14 +68,15 @@ public sealed record ConfigurationPage( string AuthenticationType, string AuthorizationType, bool IsInsecure, + bool IsTlsDisabled, bool CanInspectRuntimeDetails, IReadOnlyList Features, IReadOnlyList Subsystems, IReadOnlyList LoadedOptions, string OptionsMessage) { - public string SecurityMode => IsInsecure ? "Insecure" : "Secure"; - public string SecurityTone => IsInsecure ? "warn" : "good"; + public string SecurityMode => IsInsecure ? "Insecure" : IsTlsDisabled ? "TLS disabled" : "Secure"; + public string SecurityTone => IsInsecure || IsTlsDisabled ? "warn" : "good"; public bool HasSubsystems => Subsystems.Count > 0; public bool HasLoadedOptions => LoadedOptions.Count > 0; } diff --git a/src/EventStore.ClusterNode/Program.cs b/src/EventStore.ClusterNode/Program.cs index 4265968fc7..3ed49fe05d 100644 --- a/src/EventStore.ClusterNode/Program.cs +++ b/src/EventStore.ClusterNode/Program.cs @@ -201,6 +201,14 @@ public static async Task Main(string[] args) "INSECURE MODE WILL DISABLE ALL AUTHENTICATION, AUTHORIZATION AND TRANSPORT SECURITY FOR ALL CLIENTS AND NODES.\n" + "==============================================================================================================\n"); } + else if (options.Application.DisableTls) + { + Log.Warning( + "\n==============================================================================================================\n" + + "TLS IS DISABLED. THIS MODE IS *NOT* RECOMMENDED FOR PRODUCTION USE.\n" + + "AUTHENTICATION AND AUTHORIZATION REMAIN ENABLED, BUT TRAFFIC IS NOT ENCRYPTED.\n" + + "==============================================================================================================\n"); + } if (options.Application.WhatIf) { diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs index ce8af09856..3ec6433bfd 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs @@ -48,6 +48,7 @@ public void should_set_command_line_args_to_default_values() Assert.AreEqual(true, _options.Cluster.DiscoverViaDns, "DiscoverViaDns"); Assert.AreEqual(30, _options.Application.StatsPeriodSec, "StatsPeriod"); Assert.AreEqual(false, _options.Application.DisableHttpCaching, "DisableHTTPCaching"); + Assert.AreEqual(false, _options.Application.DisableTls, "DisableTls"); Assert.AreEqual(false, _options.Database.SkipDbVerify, "VerifyDbHash"); Assert.AreEqual(TFConsts.MinFlushDelayMs.TotalMilliseconds, _options.Database.MinFlushDelayMs, "MinFlushDelay"); Assert.AreEqual(30, _options.Database.ScavengeHistoryMaxAge, diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs index b109e260fc..d8c2c18659 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs @@ -2,7 +2,13 @@ using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; using EventStore.Common.Utils; +using EventStore.Core.Authentication; +using EventStore.Core.Authentication.DelegatedAuthentication; +using EventStore.Core.Authentication.InternalAuthentication; +using EventStore.Core.Authorization; +using EventStore.Core.Authorization.AuthorizationPolicies; using EventStore.Core.Certificates; using EventStore.Core.Tests; using EventStore.Core.Tests.Services.Transport.Tcp; @@ -117,3 +123,60 @@ public void should_throw_an_exception() Assert.IsNotNull(_caughtException); } } + +[TestFixture(typeof(LogFormat.V2), typeof(string))] +public class with_tls_disabled_and_auth_enabled : SpecificationWithDirectoryPerTestFixture +{ + private ClusterVNode _node; + private ClusterVNodeOptions _options; + + [OneTimeSetUp] + public override async Task TestFixtureSetUp() + { + await base.TestFixtureSetUp(); + + _options = new ClusterVNodeOptions() + .ReduceMemoryUsageForTests() + .RunOnDisk(PathName); + _options = _options with + { + Application = _options.Application with + { + DisableTls = true, + }, + }; + + _node = new ClusterVNode(_options, LogFormatHelper.LogFormatFactory, + new AuthenticationProviderFactory(c => + new InternalAuthenticationProviderFactory(c, _options.DefaultUser)), + new AuthorizationProviderFactory(c => new InternalAuthorizationProviderFactory( + new StaticAuthorizationPolicyRegistry([new LegacyPolicySelectorFactory( + _options.Application.AllowAnonymousEndpointAccess, + _options.Application.AllowAnonymousStreamAccess, + _options.Application.OverrideAnonymousEndpointAccessForGossip).Create(c.MainQueue)]))), + certificateProvider: new OptionsCertificateProvider()); + } + + [Test] + public void should_not_require_certificates() + { + Assert.IsNotNull(_node); + } + + [Test] + public void should_disable_transport_tls() + { + Assert.IsTrue(_node.DisableHttps); + Assert.IsTrue(_options.Application.TlsDisabled()); + Assert.AreEqual(new IPEndPoint(IPAddress.Loopback, 1112), _node.NodeInfo.InternalTcp); + } + + [Test] + public void should_keep_auth_enabled() + { + Assert.IsFalse(_options.Application.AuthDisabled()); + Assert.IsInstanceOf(_node.AuthenticationProvider); + var delegatedAuthenticationProvider = (DelegatedAuthenticationProvider)_node.AuthenticationProvider; + Assert.IsInstanceOf(delegatedAuthenticationProvider.Inner); + } +} diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs index fccc6145db..7da8e94316 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs @@ -72,6 +72,28 @@ public void valid_parameters() Assert.Empty(values); } + [Fact] + public void disable_tls_is_a_valid_parameter() + { + var options = GetOptions("--disable-tls true"); + + options.Application.DisableTls.Should().BeTrue(); + options.Application.TlsDisabled().Should().BeTrue(); + options.Application.AuthDisabled().Should().BeFalse(); + Assert.Empty(options.Unknown.Options); + } + + [Fact] + public void insecure_disables_tls_and_auth() + { + var options = GetOptions("--insecure true"); + + options.Application.Insecure.Should().BeTrue(); + options.Application.TlsDisabled().Should().BeTrue(); + options.Application.AuthDisabled().Should().BeTrue(); + Assert.Empty(options.Unknown.Options); + } + [Fact] public void stream_info_cache_scales_automatically_by_default() { diff --git a/src/EventStore.Core/Certificates/OptionsCertificateProvider.cs b/src/EventStore.Core/Certificates/OptionsCertificateProvider.cs index 7bdd81c5fc..ac2a74b37b 100644 --- a/src/EventStore.Core/Certificates/OptionsCertificateProvider.cs +++ b/src/EventStore.Core/Certificates/OptionsCertificateProvider.cs @@ -13,7 +13,7 @@ public class OptionsCertificateProvider : CertificateProvider public override LoadCertificateResult LoadCertificates(ClusterVNodeOptions options) { - if (options.Application.Insecure) + if (options.Application.TlsDisabled()) { Log.Information("Skipping reload of certificates since TLS is disabled."); return LoadCertificateResult.Skipped; diff --git a/src/EventStore.Core/ClusterVNode.cs b/src/EventStore.Core/ClusterVNode.cs index 625701159e..5d7e4cedea 100644 --- a/src/EventStore.Core/ClusterVNode.cs +++ b/src/EventStore.Core/ClusterVNode.cs @@ -264,14 +264,14 @@ public ClusterVNode(ClusterVNodeOptions options, throw new ArgumentException("InstanceId may not be empty.", nameof(instanceId)); } - if (!options.Application.Insecure) + if (!options.Application.TlsDisabled()) { ReloadCertificates(options); if (_certificateProvider?.TrustedRootCerts == null || _certificateProvider?.Certificate == null) { throw new InvalidConfigurationException( - "A certificate is required unless insecure mode (--insecure) is set."); + "A certificate is required unless TLS is disabled."); } } @@ -285,8 +285,8 @@ public ClusterVNode(ClusterVNodeOptions options, archiveOptions.Validate(); OptionsFormatter.LogConfig("Archive", archiveOptions); - var disableInternalTcpTls = options.Application.Insecure; - var disableExternalTcpTls = options.Application.Insecure; + var disableInternalTcpTls = options.Application.TlsDisabled(); + var disableExternalTcpTls = options.Application.TlsDisabled(); var nodeTcpOptions = configuration.GetSection("EventStore:TcpPlugin").Get() ?? new(); var enableExternalTcp = nodeTcpOptions.EnableExternalTcp; @@ -496,7 +496,7 @@ TFChunkDbConfig CreateDbConfig( streamExistenceFilterCheckpoint, streamExistenceFilterCheckpoint); var isSingleNode = options.Cluster.ClusterSize == 1; - _disableHttps = options.Application.Insecure; + _disableHttps = options.Application.TlsDisabled(); _enableUnixSocket = options.Interface.EnableUnixSocket; _queueStatsManager = new QueueStatsManager(); @@ -572,7 +572,7 @@ void StartSubsystems() _mainBus.Subscribe(shutdownService); _mainBus.Subscribe(shutdownService); - var uriScheme = options.Application.Insecure ? Uri.UriSchemeHttp : Uri.UriSchemeHttps; + var uriScheme = options.Application.TlsDisabled() ? Uri.UriSchemeHttp : Uri.UriSchemeHttps; var clusterDns = options.Cluster.DiscoverViaDns ? options.Cluster.ClusterDns : null; _nodeHttpClientFactory = new NodeHttpClientFactory( @@ -973,11 +973,11 @@ GossipAdvertiseInfo GetGossipAdvertiseInfo() }; // AUTHENTICATION INFRASTRUCTURE - delegate to plugins - authorizationProviderFactory ??= !options.Application.Insecure + authorizationProviderFactory ??= !options.Application.AuthDisabled() ? throw new InvalidConfigurationException( $"An {nameof(AuthorizationProviderFactory)} is required when running securely.") : new AuthorizationProviderFactory(_ => new PassthroughAuthorizationProviderFactory()); - authenticationProviderFactory ??= !options.Application.Insecure + authenticationProviderFactory ??= !options.Application.AuthDisabled() ? throw new InvalidConfigurationException( $"An {nameof(AuthenticationProviderFactory)} is required when running securely.") : new AuthenticationProviderFactory(_ => new PassthroughAuthenticationProviderFactory()); @@ -1074,21 +1074,21 @@ GossipAdvertiseInfo GetGossipAdvertiseInfo() $"The server does not support any authentication scheme supported by the '{_authenticationProvider.Name}' authentication provider."); } - if (!options.Application.Insecure) + if (!options.Application.TlsDisabled()) { //transport-level authentication providers httpAuthenticationProviders.Add( new NodeCertificateAuthenticationProvider(() => _certificateProvider.GetReservedNodeCommonName())); + } - if (options.Interface.EnableTrustedAuth) - { - httpAuthenticationProviders.Add(new TrustedHttpAuthenticationProvider()); - } + if (!options.Application.AuthDisabled() && options.Interface.EnableTrustedAuth) + { + httpAuthenticationProviders.Add(new TrustedHttpAuthenticationProvider()); + } - if (EnableUnixSocket) - { - httpAuthenticationProviders.Add(new UnixSocketAuthenticationProvider()); - } + if (!options.Application.AuthDisabled() && EnableUnixSocket) + { + httpAuthenticationProviders.Add(new UnixSocketAuthenticationProvider()); } //default authentication provider @@ -1106,7 +1106,7 @@ GossipAdvertiseInfo GetGossipAdvertiseInfo() ["projections"] = options.Projection.RunProjections != ProjectionType.None || options.DevMode.Dev, ["userManagement"] = options.Auth.AuthenticationType == Opts.AuthenticationTypeDefault && - !options.Application.Insecure + !options.Application.AuthDisabled() }, _authenticationProvider ); @@ -2235,7 +2235,7 @@ private void ReloadLogOptions(ClusterVNodeOptions options) private void ReloadCertificates(ClusterVNodeOptions options) { - if (options.Application.Insecure) + if (options.Application.TlsDisabled()) { Log.Information("Skipping reload of certificates since TLS is disabled."); return; diff --git a/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs index 1989046a3b..c893b76a6b 100644 --- a/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs +++ b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs @@ -168,6 +168,13 @@ public record ApplicationOptions [Description("Disable Authentication, Authorization and TLS on all TCP/HTTP interfaces.")] public bool Insecure { get; init; } = false; + [Description("Disable TLS on all TCP/HTTP interfaces while keeping authentication and authorization enabled.")] + public bool DisableTls { get; init; } = false; + + public bool AuthDisabled() => Insecure; + + public bool TlsDisabled() => Insecure || DisableTls; + [Description("Allow anonymous access to HTTP API endpoints.")] public bool AllowAnonymousEndpointAccess { get; init; } = false; diff --git a/src/EventStore.Core/Configuration/ClusterVNodeOptionsValidator.cs b/src/EventStore.Core/Configuration/ClusterVNodeOptionsValidator.cs index 49d890a334..f0f6c29e56 100644 --- a/src/EventStore.Core/Configuration/ClusterVNodeOptionsValidator.cs +++ b/src/EventStore.Core/Configuration/ClusterVNodeOptionsValidator.cs @@ -149,7 +149,7 @@ public static bool ValidateForStartup(ClusterVNodeOptions options) return false; } - if (options.Application.Insecure || options.Auth.AuthenticationType != Opts.AuthenticationTypeDefault) + if (options.Application.AuthDisabled() || options.Auth.AuthenticationType != Opts.AuthenticationTypeDefault) { if (options.DefaultUser.DefaultAdminPassword != SystemUsers.DefaultAdminPassword) { diff --git a/src/EventStore.Core/Telemetry/TelemetryService.cs b/src/EventStore.Core/Telemetry/TelemetryService.cs index 7630cb3a66..b7015e6e41 100644 --- a/src/EventStore.Core/Telemetry/TelemetryService.cs +++ b/src/EventStore.Core/Telemetry/TelemetryService.cs @@ -220,6 +220,7 @@ private async ValueTask Handle(TelemetryMessage.Request message, CancellationTok { ["clusterSize"] = _nodeOptions.Cluster.ClusterSize, ["insecure"] = _nodeOptions.Application.Insecure, + ["disableTls"] = _nodeOptions.Application.DisableTls, ["runProjections"] = _nodeOptions.Projection.RunProjections.ToString(), ["authorizationType"] = _nodeOptions.Auth.AuthorizationType, ["authenticationType"] = _nodeOptions.Auth.AuthenticationType