Skip to content
Open
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
20 changes: 19 additions & 1 deletion src/KubernetesClient/Kubernetes.ConfigInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

SkipTlsVerify = config.SkipTlsVerify;
TlsServerName = config.TlsServerName;
ServerCertificateCustomValidationCallback = config.ServerCertificateCustomValidationCallback;
CreateHttpClient(handlers, config);
InitializeFromConfig(config);
HttpClientTimeout = config.HttpClientTimeout;
Expand Down Expand Up @@ -72,7 +73,22 @@
{
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
var cert = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

Creating a new X509Certificate2 from an existing certificate can lead to resource leaks since the newly created certificate is not disposed. Consider using a cast with null-coalescing instead: 'var cert = certificate as X509Certificate2 ?? throw new ArgumentException("Certificate must be X509Certificate2");' or document that callers should ensure proper disposal if needed.

Suggested change
var cert = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var cert = certificate as X509Certificate2
?? throw new ArgumentException("Certificate must be X509Certificate2", nameof(certificate));

Copilot uses AI. Check for mistakes.
return config.ServerCertificateCustomValidationCallback(null, cert, chain, sslPolicyErrors);
};
#else
HttpClientHandler.ServerCertificateCustomValidationCallback = config.ServerCertificateCustomValidationCallback;
#endif
}
else if (config.SkipTlsVerify)
{
#if NET5_0_OR_GREATER
HttpClientHandler.SslOptions.RemoteCertificateValidationCallback =
Expand Down Expand Up @@ -128,6 +144,8 @@

private string TlsServerName { get; }

private Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> 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
Expand Down Expand Up @@ -220,7 +238,7 @@
#else
throw new NotSupportedException("Custom trust store requires .NET 5.0 or later. Current platform does not support this feature.");
#endif
var isValid = chain.Build((X509Certificate2)certificate);

Check warning on line 241 in src/KubernetesClient/Kubernetes.ConfigInit.cs

View workflow job for this annotation

GitHub Actions / Dotnet build (ubuntu-latest)

Unreachable code detected

Check warning on line 241 in src/KubernetesClient/Kubernetes.ConfigInit.cs

View workflow job for this annotation

GitHub Actions / Dotnet build (macOS-latest)

Unreachable code detected

Check warning on line 241 in src/KubernetesClient/Kubernetes.ConfigInit.cs

View workflow job for this annotation

GitHub Actions / Dotnet build (windows-latest)

Unreachable code detected

var isTrusted = false;

Expand Down
16 changes: 13 additions & 3 deletions src/KubernetesClient/Kubernetes.WebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,22 @@ protected async Task<WebSocket> 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)
var cert = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

Creating a new X509Certificate2 from an existing certificate can lead to resource leaks since the newly created certificate is not disposed. Consider using a cast with null-coalescing instead: 'var cert = certificate as X509Certificate2 ?? throw new ArgumentException("Certificate must be X509Certificate2");' or document that callers should ensure proper disposal if needed.

Copilot uses AI. Check for mistakes.
return this.ServerCertificateCustomValidationCallback(null, cert, chain, sslPolicyErrors);
});
}
else if (this.CaCerts != null)
{
webSocketBuilder.ExpectServerCertificate(this.CaCerts);
}

if (this.SkipTlsVerify)
else if (this.SkipTlsVerify)
{
webSocketBuilder.SkipServerCertificateValidation();
}
Expand Down
29 changes: 29 additions & 0 deletions src/KubernetesClient/KubernetesClientConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using k8s.Authentication;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace k8s
Expand Down Expand Up @@ -56,6 +57,34 @@ public partial class KubernetesClientConfiguration
/// </summary>
public bool SkipTlsVerify { get; set; }

/// <summary>
/// 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 <see cref="SkipTlsVerify"/> when set.
/// </summary>
/// <remarks>
/// <para>
/// The callback signature is: <c>Func&lt;HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool&gt;</c>.
/// Note that the HttpRequestMessage parameter may be null in some contexts (e.g., WebSocket connections).
/// </para>
/// <para>
/// Example usage to disable revocation checking:
/// </para>
/// <code>
/// config.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
/// {
/// if (errors == SslPolicyErrors.None)
/// return true;
///
/// // Disable revocation checking
/// chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
/// return chain.Build((X509Certificate2)cert);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The cast to X509Certificate2 in the documentation example is redundant since the 'cert' parameter is already typed as X509Certificate2 in the callback signature. The example should use 'cert' directly instead of casting it.

Suggested change
/// return chain.Build((X509Certificate2)cert);
/// return chain.Build(cert);

Copilot uses AI. Check for mistakes.
/// };
/// </code>
/// </remarks>
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateCustomValidationCallback { get; set; }

/// <summary>
/// Option to override the TLS server name
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/KubernetesClient/WebSocketBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Net.Security;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;

Expand Down Expand Up @@ -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
Expand Down
140 changes: 140 additions & 0 deletions tests/KubernetesClient.Tests/CustomCertificateValidationTests.cs
Original file line number Diff line number Diff line change
@@ -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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// 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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// 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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The cast to X509Certificate2 is redundant. Since 'cert' parameter in the callback is already X509Certificate2, you can pass it directly to chain.Build() without casting.

Suggested change
return chain.Build((X509Certificate2)cert);
return chain.Build(cert);

Copilot uses AI. Check for mistakes.
},
};

// Act
var client = new Kubernetes(config);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// 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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// 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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// 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);
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

This assignment to client is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

// Assert
Assert.Null(config.ServerCertificateCustomValidationCallback);
Assert.True(config.SkipTlsVerify);
}
}
}
Loading