diff --git a/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
index a2301b464..a9345b215 100644
--- a/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
+++ b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
@@ -5,6 +5,7 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
+using System.Text.Json.Nodes;
namespace k8s
{
@@ -277,6 +278,10 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
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;
+
if (!Uri.TryCreate(Host, UriKind.Absolute, out var uri))
{
throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)");
@@ -306,26 +311,28 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
{
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))
{
+ 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
+ // 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
}
}
@@ -416,7 +423,19 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
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
@@ -429,7 +448,7 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
// TODO: support client certificates here too.
if (AccessToken != null)
{
- TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
+ TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
}
}
@@ -440,16 +459,79 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
}
}
+ ///
+ /// Converts a resolved into the
+ /// spec.cluster JSON representation defined by the exec credential plugin
+ /// protocol (client.authentication.k8s.io/v1). Returns null if
+ /// is null.
+ ///
+ ///
+ /// The AOT does not include Extensions (dynamic types
+ /// are incompatible with AOT), so spec.cluster.config is not populated.
+ ///
+ ///
+ internal static JsonNode ToExecClusterInfo(ClusterEndpoint cluster)
+ {
+ 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 captureStdError = null)
+ {
+ return CreateRunnableExternalProcess(config, captureStdError, null);
+ }
+
+ public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler 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;
+ }
+ }
+
+ 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)
@@ -493,6 +575,11 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev
/// The token, client certificate data, and the client key data received from the external command execution
///
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
+ {
+ return ExecuteExternalCommand(config, null);
+ }
+
+ public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config, ClusterEndpoint cluster)
{
if (config == null)
{
@@ -500,7 +587,7 @@ public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution co
}
var captureStdError = ExecStdError;
- var process = CreateRunnableExternalProcess(config, captureStdError);
+ var process = CreateRunnableExternalProcess(config, captureStdError, cluster);
try
{
diff --git a/src/KubernetesClient/Authentication/ExecTokenProvider.cs b/src/KubernetesClient/Authentication/ExecTokenProvider.cs
index 26bc9b961..4f6487105 100644
--- a/src/KubernetesClient/Authentication/ExecTokenProvider.cs
+++ b/src/KubernetesClient/Authentication/ExecTokenProvider.cs
@@ -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()
@@ -41,7 +48,7 @@ public async Task 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);
}
}
}
diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
index ca9e206bd..d1b42686a 100644
--- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
+++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
@@ -5,11 +5,14 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
+using System.Text.Json.Nodes;
namespace k8s
{
public partial class KubernetesClientConfiguration
{
+ internal const string ExecExtensionName = "client.authentication.k8s.io/exec";
+
///
/// kubeconfig Default Location
///
@@ -277,6 +280,10 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
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;
+
if (!Uri.TryCreate(Host, UriKind.Absolute, out var uri))
{
throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)");
@@ -306,15 +313,16 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
{
if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
- var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
- var pemText = Encoding.UTF8.GetString(Convert.FromBase64String(data));
+ CertificateAuthorityData = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
+ var pemText = Encoding.UTF8.GetString(Convert.FromBase64String(CertificateAuthorityData));
SslCaCerts = CertUtils.LoadFromPemText(pemText);
}
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
{
- SslCaCerts = CertUtils.LoadPemFileCert(GetFullPath(
- k8SConfig,
- clusterDetails.ClusterEndpoint.CertificateAuthority));
+ var caBytes = File.ReadAllBytes(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority));
+ CertificateAuthorityData = Convert.ToBase64String(caBytes);
+ var pemText = Encoding.UTF8.GetString(caBytes);
+ SslCaCerts = CertUtils.LoadFromPemText(pemText);
}
}
}
@@ -426,7 +434,25 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
throw new KubeConfigException("External command execution missing ApiVersion key");
}
- var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
+ ClusterEndpoint clusterEndpoint = null;
+ if (userDetails.UserCredentials.ExternalExecution.ProvideClusterInfo)
+ {
+ var clusterDetails = k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals(
+ activeContext.ContextDetails.Cluster,
+ StringComparison.OrdinalIgnoreCase));
+ var rawCluster = clusterDetails?.ClusterEndpoint;
+
+ clusterEndpoint = new ClusterEndpoint
+ {
+ Server = this.Host,
+ SkipTlsVerify = this.SkipTlsVerify,
+ TlsServerName = this.TlsServerName,
+ CertificateAuthorityData = this.CertificateAuthorityData,
+ Extensions = rawCluster?.Extensions,
+ };
+ }
+
+ 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
@@ -439,7 +465,7 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
// TODO: support client certificates here too.
if (AccessToken != null)
{
- TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
+ TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
}
}
@@ -450,23 +476,86 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
}
}
+ ///
+ /// Converts a (kubeconfig model) into the
+ /// spec.cluster JSON representation defined by the exec credential plugin
+ /// protocol (client.authentication.k8s.io/v1). Returns null if
+ /// is null.
+ ///
+ ///
+ internal static JsonNode ToExecClusterInfo(ClusterEndpoint cluster)
+ {
+ 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;
+ }
+
+ var execExtension = cluster.Extensions?
+ .FirstOrDefault(e => e.Name == ExecExtensionName);
+ if (execExtension != null)
+ {
+ object extConfig = execExtension.Extension;
+ if (extConfig != null)
+ {
+ node["config"] = JsonSerializer.SerializeToNode(extConfig);
+ }
+ }
+
+ return node;
+ }
+
public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler captureStdError = null)
+ {
+ return CreateRunnableExternalProcess(config, captureStdError, null);
+ }
+
+ public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler captureStdError, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
- var execInfo = new Dictionary
+ var spec = new JsonObject { ["interactive"] = Environment.UserInteractive };
+ if (config.ProvideClusterInfo)
{
- { "apiVersion", config.ApiVersion },
- { "kind", "ExecCredentials" },
- { "spec", new Dictionary { { "interactive", Environment.UserInteractive } } },
+ var clusterNode = ToExecClusterInfo(cluster);
+ if (clusterNode != null)
+ {
+ spec["cluster"] = clusterNode;
+ }
+ }
+
+ var execInfo = new JsonObject
+ {
+ ["apiVersion"] = config.ApiVersion,
+ ["kind"] = "ExecCredentials",
+ ["spec"] = spec,
};
var process = new Process();
- process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", JsonSerializer.Serialize(execInfo));
+ process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", execInfo.ToJsonString());
if (config.EnvironmentVariables != null)
{
foreach (var configEnvironmentVariable in config.EnvironmentVariables)
@@ -510,6 +599,11 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev
/// The token, client certificate data, and the client key data received from the external command execution
///
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
+ {
+ return ExecuteExternalCommand(config, null);
+ }
+
+ public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config, ClusterEndpoint cluster)
{
if (config == null)
{
@@ -517,7 +611,7 @@ public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution co
}
var captureStdError = ExecStdError;
- var process = CreateRunnableExternalProcess(config, captureStdError);
+ var process = CreateRunnableExternalProcess(config, captureStdError, cluster);
try
{
diff --git a/src/KubernetesClient/KubernetesClientConfiguration.cs b/src/KubernetesClient/KubernetesClientConfiguration.cs
index 6d05c913a..780230175 100644
--- a/src/KubernetesClient/KubernetesClientConfiguration.cs
+++ b/src/KubernetesClient/KubernetesClientConfiguration.cs
@@ -61,6 +61,12 @@ public partial class KubernetesClientConfiguration
///
public string TlsServerName { get; set; }
+ ///
+ /// Gets the base64-encoded PEM certificate authority data, resolved from either
+ /// inline data or file path during cluster configuration.
+ ///
+ public string CertificateAuthorityData { get; set; }
+
///
/// Gets or sets the HTTP user agent.
///
diff --git a/tests/KubernetesClient.Tests/ExternalExecutionTests.cs b/tests/KubernetesClient.Tests/ExternalExecutionTests.cs
index f4b101f53..ea6b7a3b6 100644
--- a/tests/KubernetesClient.Tests/ExternalExecutionTests.cs
+++ b/tests/KubernetesClient.Tests/ExternalExecutionTests.cs
@@ -1,6 +1,6 @@
using k8s.KubeConfigModels;
using System.Collections.Generic;
-using System.Text.Json;
+using System.Text.Json.Nodes;
using Xunit;
namespace k8s.Tests
@@ -19,13 +19,192 @@ public void CreateRunnableExternalProcess()
{ new Dictionary { { "name", "testkey" }, { "value", "testvalue" } } },
});
- var actualExecInfo = JsonSerializer.Deserialize>(actual.StartInfo.EnvironmentVariables["KUBERNETES_EXEC_INFO"]);
- Assert.Equal("testingversion", actualExecInfo["apiVersion"].ToString());
- Assert.Equal("ExecCredentials", actualExecInfo["kind"].ToString());
+ var json = JsonNode.Parse(actual.StartInfo.EnvironmentVariables["KUBERNETES_EXEC_INFO"]);
+ Assert.Equal("testingversion", json["apiVersion"]?.GetValue());
+ Assert.Equal("ExecCredentials", json["kind"]?.GetValue());
+ Assert.False(json["spec"].AsObject().ContainsKey("cluster"));
Assert.Equal("command", actual.StartInfo.FileName);
Assert.Equal("arg1 arg2", actual.StartInfo.Arguments);
Assert.Equal("testvalue", actual.StartInfo.EnvironmentVariables["testkey"]);
}
+
+ [Fact]
+ public void ToExecClusterInfoMapsFieldsCorrectly()
+ {
+ var cluster = new ClusterEndpoint
+ {
+ Server = "https://my-cluster.example.com:6443",
+ CertificateAuthorityData = "LS0tLS1CRUdJTi0t",
+ SkipTlsVerify = false,
+ TlsServerName = "my-cluster.example.com",
+ Extensions = new List
+ {
+ new NamedExtension
+ {
+ Name = KubernetesClientConfiguration.ExecExtensionName,
+ Extension = new Dictionary