From d1fd6c89a0c1225ba05fe78d23f05295d57a4250 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:19:54 +0000 Subject: [PATCH 1/5] Initial plan From 0308f53c9132b11fd16036531e52e5f4f8bb1b1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:30:01 +0000 Subject: [PATCH 2/5] Add ServerCertificateCustomValidationCallback property for custom TLS validation Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- src/KubernetesClient/Kubernetes.ConfigInit.cs | 19 ++++++++++++++- src/KubernetesClient/Kubernetes.WebSocket.cs | 15 +++++++++--- .../KubernetesClientConfiguration.cs | 23 +++++++++++++++++++ src/KubernetesClient/WebSocketBuilder.cs | 9 ++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/KubernetesClient/Kubernetes.ConfigInit.cs b/src/KubernetesClient/Kubernetes.ConfigInit.cs index e87e4e96a..9a2f3543f 100644 --- a/src/KubernetesClient/Kubernetes.ConfigInit.cs +++ b/src/KubernetesClient/Kubernetes.ConfigInit.cs @@ -37,6 +37,7 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler SkipTlsVerify = config.SkipTlsVerify; TlsServerName = config.TlsServerName; + ServerCertificateCustomValidationCallback = config.ServerCertificateCustomValidationCallback; CreateHttpClient(handlers, config); InitializeFromConfig(config); HttpClientTimeout = config.HttpClientTimeout; @@ -72,7 +73,21 @@ private void InitializeFromConfig(KubernetesClientConfiguration config) { if (BaseUri.Scheme == "https") { - if (config.SkipTlsVerify) + // Custom validation callback takes precedence + if (config.ServerCertificateCustomValidationCallback != null) + { +#if NET5_0_OR_GREATER + HttpClientHandler.SslOptions.RemoteCertificateValidationCallback = + (sender, certificate, chain, sslPolicyErrors) => + { + // RemoteCertificateValidationCallback doesn't provide HttpRequestMessage, so pass null + return config.ServerCertificateCustomValidationCallback(null, (X509Certificate2)certificate, chain, sslPolicyErrors); + }; +#else + HttpClientHandler.ServerCertificateCustomValidationCallback = config.ServerCertificateCustomValidationCallback; +#endif + } + else if (config.SkipTlsVerify) { #if NET5_0_OR_GREATER HttpClientHandler.SslOptions.RemoteCertificateValidationCallback = @@ -128,6 +143,8 @@ private void InitializeFromConfig(KubernetesClientConfiguration config) private string TlsServerName { get; } + private Func ServerCertificateCustomValidationCallback { get; } + // NOTE: this method replicates the logic that the base ServiceClient uses except that it doesn't insert the RetryDelegatingHandler // and it does insert the WatcherDelegatingHandler. we don't want the RetryDelegatingHandler because it has a very broad definition // of what requests have failed. it considers everything outside 2xx to be failed, including 1xx (e.g. 101 Switching Protocols) and diff --git a/src/KubernetesClient/Kubernetes.WebSocket.cs b/src/KubernetesClient/Kubernetes.WebSocket.cs index aeca29708..79eabfedc 100644 --- a/src/KubernetesClient/Kubernetes.WebSocket.cs +++ b/src/KubernetesClient/Kubernetes.WebSocket.cs @@ -259,12 +259,21 @@ protected async Task StreamConnectAsync(Uri uri, string webSocketSubP } } - if (this.CaCerts != null) + // Custom validation callback takes precedence + if (this.ServerCertificateCustomValidationCallback != null) + { + webSocketBuilder.SetServerCertificateCustomValidationCallback( + (sender, certificate, chain, sslPolicyErrors) => + { + // Convert to the expected signature (with HttpRequestMessage as first parameter) + return this.ServerCertificateCustomValidationCallback(null, (X509Certificate2)certificate, chain, sslPolicyErrors); + }); + } + else if (this.CaCerts != null) { webSocketBuilder.ExpectServerCertificate(this.CaCerts); } - - if (this.SkipTlsVerify) + else if (this.SkipTlsVerify) { webSocketBuilder.SkipServerCertificateValidation(); } diff --git a/src/KubernetesClient/KubernetesClientConfiguration.cs b/src/KubernetesClient/KubernetesClientConfiguration.cs index ac4a66719..9d7f03697 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.cs @@ -1,5 +1,6 @@ using k8s.Authentication; using System.Net.Http; +using System.Net.Security; using System.Security.Cryptography.X509Certificates; namespace k8s @@ -56,6 +57,28 @@ public partial class KubernetesClientConfiguration /// public bool SkipTlsVerify { get; set; } + /// + /// Gets or sets a custom server certificate validation callback. + /// This allows fine-grained control over certificate validation, such as disabling + /// revocation checks while maintaining other security validations. + /// Takes precedence over when set. + /// + /// + /// Example usage to disable revocation checking: + /// + /// config.ServerCertificateCustomValidationCallback = (sender, cert, chain, errors) => + /// { + /// if (errors == SslPolicyErrors.None) + /// return true; + /// + /// // Disable revocation checking + /// chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + /// return chain.Build((X509Certificate2)cert); + /// }; + /// + /// + public Func ServerCertificateCustomValidationCallback { get; set; } + /// /// Option to override the TLS server name /// diff --git a/src/KubernetesClient/WebSocketBuilder.cs b/src/KubernetesClient/WebSocketBuilder.cs index 8acc3c5ce..9c962e645 100644 --- a/src/KubernetesClient/WebSocketBuilder.cs +++ b/src/KubernetesClient/WebSocketBuilder.cs @@ -1,3 +1,4 @@ +using System.Net.Security; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; @@ -44,6 +45,14 @@ public WebSocketBuilder ExpectServerCertificate(X509Certificate2Collection serve return this; } + public WebSocketBuilder SetServerCertificateCustomValidationCallback(RemoteCertificateValidationCallback callback) + { +#if NETSTANDARD2_1 || NET5_0_OR_GREATER + Options.RemoteCertificateValidationCallback = callback; +#endif + return this; + } + public WebSocketBuilder SkipServerCertificateValidation() { #if NETSTANDARD2_1 || NET5_0_OR_GREATER From 990bbdde0433877f204b7d58012d323d5bacb039 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:35:12 +0000 Subject: [PATCH 3/5] Add tests for custom certificate validation callback Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- .../CustomCertificateValidationTests.cs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/KubernetesClient.Tests/CustomCertificateValidationTests.cs diff --git a/tests/KubernetesClient.Tests/CustomCertificateValidationTests.cs b/tests/KubernetesClient.Tests/CustomCertificateValidationTests.cs new file mode 100644 index 000000000..7b283d9db --- /dev/null +++ b/tests/KubernetesClient.Tests/CustomCertificateValidationTests.cs @@ -0,0 +1,140 @@ +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Xunit; + +namespace k8s.Tests +{ + public class CustomCertificateValidationTests + { + [Fact] + public void CustomValidationCallbackShouldBeUsedWhenSet() + { + // Arrange + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => + { + return true; + }, + }; + + // Act + var client = new Kubernetes(config); + + // Assert - verify the callback was set + Assert.NotNull(config.ServerCertificateCustomValidationCallback); + } + + [Fact] + public void CustomValidationCallbackTakesPrecedenceOverSkipTlsVerify() + { + // Arrange + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + SkipTlsVerify = true, + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => + { + return false; // Custom callback returns false + }, + }; + + // Act + var client = new Kubernetes(config); + + // Assert - The custom callback should be set, not the skip all validation + Assert.NotNull(config.ServerCertificateCustomValidationCallback); + Assert.True(config.SkipTlsVerify); // SkipTlsVerify should still be true in config + } + + [Fact] + public void CustomValidationCallbackCanDisableRevocationCheck() + { + // Arrange + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => + { + // Example: Disable revocation checking + if (errors == SslPolicyErrors.None) + { + return true; + } + + // Disable revocation checking + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + return chain.Build((X509Certificate2)cert); + }, + }; + + // Act + var client = new Kubernetes(config); + + // Assert + Assert.NotNull(config.ServerCertificateCustomValidationCallback); + } + + [Fact] + public void CustomValidationCallbackCanPerformCustomLogic() + { + // Arrange + var allowedThumbprint = "1234567890ABCDEF"; + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => + { + // Example: Pin to a specific certificate thumbprint + if (cert != null && cert.Thumbprint == allowedThumbprint) + { + return true; + } + + return errors == SslPolicyErrors.None; + }, + }; + + // Act + var client = new Kubernetes(config); + + // Assert + Assert.NotNull(config.ServerCertificateCustomValidationCallback); + } + + [Fact] + public void ConfigurationWithoutCustomCallbackUsesDefaultBehavior() + { + // Arrange + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + }; + + // Act + var client = new Kubernetes(config); + + // Assert + Assert.Null(config.ServerCertificateCustomValidationCallback); + } + + [Fact] + public void SkipTlsVerifyWorksWhenNoCustomCallbackSet() + { + // Arrange + var config = new KubernetesClientConfiguration + { + Host = "https://test.example.com", + SkipTlsVerify = true, + }; + + // Act + var client = new Kubernetes(config); + + // Assert + Assert.Null(config.ServerCertificateCustomValidationCallback); + Assert.True(config.SkipTlsVerify); + } + } +} From fe6d9399ea3d2b5b5a5a4c8ec2dc14be66785ad9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:41:17 +0000 Subject: [PATCH 4/5] Update documentation to clarify HttpRequestMessage may be null Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- src/KubernetesClient/KubernetesClientConfiguration.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/KubernetesClient/KubernetesClientConfiguration.cs b/src/KubernetesClient/KubernetesClientConfiguration.cs index 9d7f03697..876e9f0ed 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.cs @@ -64,9 +64,15 @@ public partial class KubernetesClientConfiguration /// Takes precedence over when set. /// /// + /// + /// The callback signature is: Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool>. + /// Note that the HttpRequestMessage parameter may be null in some contexts (e.g., WebSocket connections). + /// + /// /// Example usage to disable revocation checking: + /// /// - /// config.ServerCertificateCustomValidationCallback = (sender, cert, chain, errors) => + /// config.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => /// { /// if (errors == SslPolicyErrors.None) /// return true; From d6f2169101a558de4c7b35c8378317774ee38b13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:45:48 +0000 Subject: [PATCH 5/5] Add defensive null checks for certificate casting Co-authored-by: tg123 <170430+tg123@users.noreply.github.com> --- src/KubernetesClient/Kubernetes.ConfigInit.cs | 3 ++- src/KubernetesClient/Kubernetes.WebSocket.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/KubernetesClient/Kubernetes.ConfigInit.cs b/src/KubernetesClient/Kubernetes.ConfigInit.cs index 9a2f3543f..134496191 100644 --- a/src/KubernetesClient/Kubernetes.ConfigInit.cs +++ b/src/KubernetesClient/Kubernetes.ConfigInit.cs @@ -81,7 +81,8 @@ private void InitializeFromConfig(KubernetesClientConfiguration config) (sender, certificate, chain, sslPolicyErrors) => { // RemoteCertificateValidationCallback doesn't provide HttpRequestMessage, so pass null - return config.ServerCertificateCustomValidationCallback(null, (X509Certificate2)certificate, chain, sslPolicyErrors); + var cert = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + return config.ServerCertificateCustomValidationCallback(null, cert, chain, sslPolicyErrors); }; #else HttpClientHandler.ServerCertificateCustomValidationCallback = config.ServerCertificateCustomValidationCallback; diff --git a/src/KubernetesClient/Kubernetes.WebSocket.cs b/src/KubernetesClient/Kubernetes.WebSocket.cs index 79eabfedc..90a08e098 100644 --- a/src/KubernetesClient/Kubernetes.WebSocket.cs +++ b/src/KubernetesClient/Kubernetes.WebSocket.cs @@ -266,7 +266,8 @@ protected async Task StreamConnectAsync(Uri uri, string webSocketSubP (sender, certificate, chain, sslPolicyErrors) => { // Convert to the expected signature (with HttpRequestMessage as first parameter) - return this.ServerCertificateCustomValidationCallback(null, (X509Certificate2)certificate, chain, sslPolicyErrors); + var cert = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + return this.ServerCertificateCustomValidationCallback(null, cert, chain, sslPolicyErrors); }); } else if (this.CaCerts != null)