From f1e2e39b09c3fad3d888731214b1765e1befe375 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:45:48 +0000 Subject: [PATCH 1/3] Initial plan From 99bc5fac0b2797b0a8f2f6155dd5eab1dc0e26d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:55:54 +0000 Subject: [PATCH 2/3] Add support for tls-server-name field in kubeconfig Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- .../kubernetes/client/openapi/ApiClient.java | 38 ++++++++++++++++++- .../kubernetes/client/util/ClientBuilder.java | 19 ++++++++++ .../io/kubernetes/client/util/KubeConfig.java | 4 ++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/kubernetes/src/main/java/io/kubernetes/client/openapi/ApiClient.java b/kubernetes/src/main/java/io/kubernetes/client/openapi/ApiClient.java index d5d3fd68c2..a64a91072a 100644 --- a/kubernetes/src/main/java/io/kubernetes/client/openapi/ApiClient.java +++ b/kubernetes/src/main/java/io/kubernetes/client/openapi/ApiClient.java @@ -85,6 +85,7 @@ public class ApiClient { private InputStream sslCaCert; private boolean verifyingSsl; private KeyManager[] keyManagers; + private String tlsServerName; private OkHttpClient httpClient; private JSON json; @@ -301,6 +302,29 @@ public ApiClient setKeyManagers(KeyManager[] managers) { return this; } + /** + * Get TLS server name for SNI (Server Name Indication). + * + * @return The TLS server name + */ + public String getTlsServerName() { + return tlsServerName; + } + + /** + * Set TLS server name for SNI (Server Name Indication). + * This is used to verify the server certificate against a specific hostname + * instead of the hostname in the URL. + * + * @param tlsServerName The TLS server name to use for certificate verification + * @return ApiClient + */ + public ApiClient setTlsServerName(String tlsServerName) { + this.tlsServerName = tlsServerName; + applySslSettings(); + return this; + } + /** *

Getter for the field dateFormat.

* @@ -1539,7 +1563,19 @@ public boolean verify(String hostname, SSLSession session) { trustManagerFactory.init(caKeyStore); } trustManagers = trustManagerFactory.getTrustManagers(); - hostnameVerifier = OkHostnameVerifier.INSTANCE; + // If tlsServerName is set, use a custom hostname verifier that checks against + // the specified server name instead of the actual hostname in the URL + if (tlsServerName != null && !tlsServerName.isEmpty()) { + hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + // Verify the certificate against tlsServerName instead of the actual hostname + return OkHostnameVerifier.INSTANCE.verify(tlsServerName, session); + } + }; + } else { + hostnameVerifier = OkHostnameVerifier.INSTANCE; + } } SSLContext sslContext = SSLContext.getInstance("TLS"); diff --git a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java index 28c737a662..ae0bfb4011 100644 --- a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java +++ b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java @@ -64,6 +64,7 @@ public class ClientBuilder { private boolean verifyingSsl = true; private Authentication authentication; private String keyStorePassphrase; + private String tlsServerName; // defaulting client protocols to HTTP1.1 and HTTP 2 private List protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1); // default to unlimited read timeout @@ -312,6 +313,11 @@ public static ClientBuilder kubeconfig(KubeConfig config, Duration tokenRefreshP } builder.setVerifyingSsl(config.verifySSL()); + String tlsServerName = config.getTlsServerName(); + if (tlsServerName != null && !tlsServerName.isEmpty()) { + builder.setTlsServerName(tlsServerName); + } + builder.setBasePath(server); builder.setAuthentication(new KubeconfigAuthentication(config, tokenRefreshPeriod)); return builder; @@ -436,6 +442,15 @@ public ClientBuilder setKeyStorePassphrase(String keyStorePassphrase) { return this; } + public String getTlsServerName() { + return tlsServerName; + } + + public ClientBuilder setTlsServerName(String tlsServerName) { + this.tlsServerName = tlsServerName; + return this; + } + public ApiClient getApiClient() { return this.apiClient; } @@ -466,6 +481,10 @@ public ApiClient build() { client.setVerifyingSsl(verifyingSsl); + if (tlsServerName != null && !tlsServerName.isEmpty()) { + client.setTlsServerName(tlsServerName); + } + if (authentication != null) { if (StringUtils.isNotEmpty(keyStorePassphrase)) { if (authentication instanceof KubeconfigAuthentication) { diff --git a/util/src/main/java/io/kubernetes/client/util/KubeConfig.java b/util/src/main/java/io/kubernetes/client/util/KubeConfig.java index ba4fb2a995..18f64f251b 100644 --- a/util/src/main/java/io/kubernetes/client/util/KubeConfig.java +++ b/util/src/main/java/io/kubernetes/client/util/KubeConfig.java @@ -176,6 +176,10 @@ public String getCertificateAuthorityFile() { return getData(currentCluster, "certificate-authority"); } + public String getTlsServerName() { + return getData(currentCluster, "tls-server-name"); + } + public String getClientCertificateFile() { return getData(currentUser, "client-certificate"); } From 2f9f511ee80471a36471991e6d7873226ea49961 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:01:58 +0000 Subject: [PATCH 3/3] Add unit tests for tls-server-name functionality Co-authored-by: brendandburns <5751682+brendandburns@users.noreply.github.com> --- .../client/util/ClientBuilderTest.java | 20 ++++++++++++ .../client/util/KubeConfigTest.java | 31 +++++++++++++++++++ .../test/resources/kubeconfig-tls-server-name | 17 ++++++++++ 3 files changed, 68 insertions(+) create mode 100644 util/src/test/resources/kubeconfig-tls-server-name diff --git a/util/src/test/java/io/kubernetes/client/util/ClientBuilderTest.java b/util/src/test/java/io/kubernetes/client/util/ClientBuilderTest.java index cfc5f054d9..bbecb6068e 100644 --- a/util/src/test/java/io/kubernetes/client/util/ClientBuilderTest.java +++ b/util/src/test/java/io/kubernetes/client/util/ClientBuilderTest.java @@ -48,6 +48,8 @@ class ClientBuilderTest { Resources.getResource("kubeconfig-https").getPath(); private static final String KUBECONFIG_HTTPS_X509_FILE_PATH = Resources.getResource("kubeconfig-https-x509").getPath(); + private static final String KUBECONFIG_TLS_SERVER_NAME_FILE_PATH = + Resources.getResource("kubeconfig-tls-server-name").getPath(); private static final String SSL_CA_CERT_PATH = new File(Resources.getResource("ca-cert.pem").getPath()).toString(); private static final String INVALID_SSL_CA_CERT_PATH = @@ -321,4 +323,22 @@ void detectsServerNotSet() { ClientBuilder.kubeconfig(kubeConfigWithoutServer); }).hasMessage("No server in kubeconfig").isInstanceOf(IllegalArgumentException.class); } + + @Test + void tlsServerNameSetFromKubeConfig() throws IOException { + ApiClient client = + ClientBuilder.kubeconfig( + KubeConfig.loadKubeConfig(new FileReader(KUBECONFIG_TLS_SERVER_NAME_FILE_PATH))) + .build(); + assertThat(client.getTlsServerName()).isEqualTo("my-cluster.example.com"); + } + + @Test + void tlsServerNameNotSetWhenNotInKubeConfig() throws IOException { + ApiClient client = + ClientBuilder.kubeconfig( + KubeConfig.loadKubeConfig(new FileReader(KUBECONFIG_HTTPS_FILE_PATH))) + .build(); + assertThat(client.getTlsServerName()).isNull(); + } } diff --git a/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java b/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java index d855283de0..6ac8ca57d1 100644 --- a/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java +++ b/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java @@ -425,4 +425,35 @@ void execCredentialsCertificate() { assertThat(kc.getCredentials()).containsEntry(KubeConfig.CRED_CLIENT_KEY_DATA_KEY, "key"); assertThat(kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY)).isNull(); } + + @Test + void tlsServerName() { + String configWithTlsServerName = + "apiVersion: v1\n" + + "clusters:\n" + + "- cluster:\n" + + " server: https://192.168.1.1:6443\n" + + " tls-server-name: my-cluster.example.com\n" + + " certificate-authority-data: dGVzdAo=\n" + + " name: test-cluster\n" + + "users:\n" + + "- user:\n" + + " token: test-token\n" + + " name: test-user\n" + + "contexts:\n" + + "- context:\n" + + " cluster: test-cluster\n" + + " user: test-user\n" + + " name: test-context\n" + + "current-context: test-context\n"; + + KubeConfig config = KubeConfig.loadKubeConfig(new StringReader(configWithTlsServerName)); + assertThat("my-cluster.example.com").isEqualTo(config.getTlsServerName()); + } + + @Test + void tlsServerNameNotPresent() { + KubeConfig config = KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_TOKEN)); + assertThat(config.getTlsServerName()).isNull(); + } } diff --git a/util/src/test/resources/kubeconfig-tls-server-name b/util/src/test/resources/kubeconfig-tls-server-name new file mode 100644 index 0000000000..41493a063f --- /dev/null +++ b/util/src/test/resources/kubeconfig-tls-server-name @@ -0,0 +1,17 @@ +apiVersion: v1 +clusters: + - cluster: + server: https://192.168.1.1:6443 + tls-server-name: my-cluster.example.com + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTXpNMU1qYzFPVGN3SGhjTk1qUXhNREkzTVRnek1ETTNXaGNOTXpReE1ESTFNVGd6TURNMwpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTXpNMU1qYzFPVGN3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUMmY5dzdYZEJLRktDN2ptQUxYaDFlNVgyWjNYL0h5K2JYR0MxV1VoeHUKS3BZUUJ5Q0oxcU5RUjhQWHhOWEdvd3BJUFp0ekU0T2NQdjFRUlJyd0hvNkZvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVXdYam5rdWRSek55T1N2VmY2Mm1PCnFqc2FxREF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnRGRYelNuYTRnS2x6akJ2MllLVDJqM0VlSzY1WndPa2cKNGMxaG1nK29CVHdDSVFEdTBGR2NjRVE3OXE2U0pGTkF1bUhsd0JaY3lmeE5sOWhDb3hSRUdWVy93Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: test-cluster +users: + - user: + token: test-token + name: test-user +contexts: + - context: + cluster: test-cluster + user: test-user + name: test-context +current-context: test-context