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
113 changes: 100 additions & 13 deletions src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;

namespace k8s
{
Expand Down Expand Up @@ -277,6 +278,10 @@
SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify;
TlsServerName = clusterDetails.ClusterEndpoint.TlsServerName;

// Reset CA data so it always reflects the cluster currently being resolved
// and is never carried over from a prior state.
CertificateAuthorityData = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

may i know reason why this is introduced?


if (!Uri.TryCreate(Host, UriKind.Absolute, out var uri))
{
throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)");
Expand Down Expand Up @@ -306,26 +311,28 @@
{
if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
CertificateAuthorityData = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
#if NET9_0_OR_GREATER
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(data)));
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(CertificateAuthorityData)));
#else
string nullPassword = null;
// This null password is to change the constructor to fix this KB:
// https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data), nullPassword));
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(CertificateAuthorityData), nullPassword));
#endif
}
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
{
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
var caPath = GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority);
CertificateAuthorityData = Convert.ToBase64String(File.ReadAllBytes(caPath));

// File-path loaders auto-detect cert format (PEM/DER/PFX), which the
// byte-based APIs do not reliably do on pre-.NET 9. The second read is
Comment on lines +326 to +330

@mutsaddi-deshaw mutsaddi-deshaw Jun 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Can a non-PEM CA file can realistically occur? kubeconfig CA files are PEM per spec, and I think such a file wouldn't work with kubectl or client-go either. client-go appears to forward the raw bytes unchanged and the non-AOT C# client also seems to assume PEM (CertUtils.LoadPemFileCert). Do we have a real scenario that produces a DER/PFX CA in a kubeconfig? If not, I'd rather not add handling for input the spec disallows.

// intentional to preserve format flexibility for SslCaCerts.
#if NET9_0_OR_GREATER
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificateFromFile(GetFullPath(
k8SConfig,
clusterDetails.ClusterEndpoint.CertificateAuthority)));
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificateFromFile(caPath));
#else
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath(
k8SConfig,
clusterDetails.ClusterEndpoint.CertificateAuthority)));
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(caPath));
#endif
}
}
Expand Down Expand Up @@ -416,7 +423,19 @@
throw new KubeConfigException("External command execution missing ApiVersion key");
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
ClusterEndpoint clusterEndpoint = null;
if (userDetails.UserCredentials.ExternalExecution.ProvideClusterInfo)
{
clusterEndpoint = new ClusterEndpoint
{
Server = this.Host,
SkipTlsVerify = this.SkipTlsVerify,
TlsServerName = this.TlsServerName,
CertificateAuthorityData = this.CertificateAuthorityData,
};
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
AccessToken = response.Status.Token;
// When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx)
// expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external
Expand All @@ -429,7 +448,7 @@
// TODO: support client certificates here too.
if (AccessToken != null)
{
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
}
}

Expand All @@ -440,16 +459,79 @@
}
}

/// <summary>
/// Converts a resolved <see cref="ClusterEndpoint"/> into the
/// <c>spec.cluster</c> JSON representation defined by the exec credential plugin
/// protocol (client.authentication.k8s.io/v1). Returns <c>null</c> if
/// <paramref name="cluster"/> is <c>null</c>.
/// </summary>
/// <remarks>
/// The AOT <see cref="ClusterEndpoint"/> does not include Extensions (dynamic types
/// are incompatible with AOT), so <c>spec.cluster.config</c> is not populated.
/// </remarks>
/// <seealso href="https://kubernetes.io/docs/reference/config-api/client-authentication.v1/#Cluster"/>
internal static JsonNode ToExecClusterInfo(ClusterEndpoint cluster)

Check warning on line 473 in src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs

View workflow job for this annotation

GitHub Actions / e2e

Check warning on line 473 in src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs

View workflow job for this annotation

GitHub Actions / e2e

{
if (cluster == null)
{
return null;
}

var node = new JsonObject
{
["server"] = cluster.Server,
};

if (cluster.SkipTlsVerify)
{
node["insecure-skip-tls-verify"] = true;
}

if (!string.IsNullOrEmpty(cluster.CertificateAuthorityData))
{
node["certificate-authority-data"] = cluster.CertificateAuthorityData;
}

if (!string.IsNullOrEmpty(cluster.TlsServerName))
{
node["tls-server-name"] = cluster.TlsServerName;
}

return node;
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError = null)
{
return CreateRunnableExternalProcess(config, captureStdError, null);
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var spec = new JsonObject { ["interactive"] = Environment.UserInteractive };
if (config.ProvideClusterInfo)
{
var clusterNode = ToExecClusterInfo(cluster);
if (clusterNode != null)
{
spec["cluster"] = clusterNode;
}
}
Comment thread
mutsaddi-deshaw marked this conversation as resolved.

var execInfo = new JsonObject
{
["apiVersion"] = config.ApiVersion,
["kind"] = "ExecCredentials",
["spec"] = spec,
};

var process = new Process();

process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}");
process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", execInfo.ToJsonString());
if (config.EnvironmentVariables != null)
{
foreach (var configEnvironmentVariable in config.EnvironmentVariables)
Expand Down Expand Up @@ -493,14 +575,19 @@
/// The token, client certificate data, and the client key data received from the external command execution
/// </returns>
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
{
return ExecuteExternalCommand(config, null);
}

public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var captureStdError = ExecStdError;
var process = CreateRunnableExternalProcess(config, captureStdError);
var process = CreateRunnableExternalProcess(config, captureStdError, cluster);

try
{
Expand Down
9 changes: 8 additions & 1 deletion src/KubernetesClient/Authentication/ExecTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ namespace k8s.Authentication
public class ExecTokenProvider : ITokenProvider
{
private readonly ExternalExecution exec;
private readonly ClusterEndpoint cluster;
private ExecCredentialResponse response;

public ExecTokenProvider(ExternalExecution exec)
: this(exec, null)
{
}

public ExecTokenProvider(ExternalExecution exec, ClusterEndpoint cluster)
{
this.exec = exec;
this.cluster = cluster;
}

private bool NeedsRefresh()
Expand Down Expand Up @@ -41,7 +48,7 @@ public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(Cancel
private async Task RefreshToken()
{
response =
await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec)).ConfigureAwait(false);
await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec, this.cluster)).ConfigureAwait(false);
}
}
}
Loading
Loading