Skip to content

fix(httpclient): enforce tlsServerName certificate verification in Jetty, fail loud in other clients - #7956

Open
GrosQuildu wants to merge 2 commits into
fabric8io:mainfrom
GrosQuildu:fix/tls-server-name-cert-verification
Open

fix(httpclient): enforce tlsServerName certificate verification in Jetty, fail loud in other clients#7956
GrosQuildu wants to merge 2 commits into
fabric8io:mainfrom
GrosQuildu:fix/tls-server-name-cert-verification

Conversation

@GrosQuildu

@GrosQuildu GrosQuildu commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

Fabric8 loads kubeconfig tls-server-name and passes it to the configured HTTP client builder. Kubernetes uses this field as the expected server certificate name when the URL host is not the Kubernetes API server certificate identity.

Fabric8 does not enforce that behavior. JDK, OkHttp, Vert.x, and Vert.x 5 warn that tlsServerName is unsupported and continue with normal URL-host verification. Jetty uses tlsServerName only as Server Name Indication (SNI) in JettyHttpClientBuilder. SNI selects a server certificate on the remote endpoint, but it does not change the client-side certificate hostname verification reference identity.

As a result, a Fabric8 client can send Kubernetes credentials to a TLS endpoint whose certificate is valid for the URL host but invalid for the configured tls-server-name.

Exploit Scenario

A CI service uses Fabric8 with a kubeconfig whose cluster URL points at a local tunnel, proxy endpoint, or IP address, and whose tls-server-name contains the real Kubernetes API certificate identity. An attacker controls the endpoint reached by the URL host and presents a certificate that is trusted for that URL host but not for the Kubernetes API server name. Fabric8 accepts the TLS connection and sends the caller's Kubernetes credentials to the wrong endpoint.

This Java PoC uses Fabric8's Jetty HTTP client and HTTPS mock server. The mock server certificate is trusted by the client and is valid for localhost, but not for api.example.cluster.local. The vulnerable behavior is that the request still succeeds and the mock server receives the bearer token.

import io.fabric8.kubernetes.client.http.HttpResponse;
import io.fabric8.kubernetes.client.internal.SSLUtils;
import io.fabric8.kubernetes.client.jetty.JettyHttpClientFactory;
import io.fabric8.mockwebserver.DefaultMockServer;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class JettyTlsServerNamePocTest {

  @Test
  void tlsServerNameDoesNotProtectAuthorizationHeader() throws Exception {
    DefaultMockServer tlsServer = new DefaultMockServer(true);
    tlsServer.start();
    try {
      tlsServer.expect()
          .get()
          .withPath("/api/v1/pods")
          .andReturn(200, "ok")
          .once();

      TrustManager[] trustManagers = SSLUtils.trustManagers(
          null, tlsServer.getSelfSignedCertificate().certificatePath(), false, null, null);

      try (var client = new JettyHttpClientFactory().newBuilder()
          .sslContext(null, trustManagers)
          .tlsServerName("api.example.cluster.local")
          .build()) {
        var request = client.newHttpRequestBuilder()
            .uri(tlsServer.url("/api/v1/pods"))
            .setHeader("Authorization", "Bearer synthetic-test-token")
            .build();

        HttpResponse<String> response = client.sendAsync(request, String.class)
            .get(10, TimeUnit.SECONDS);

        assertThat(response.code()).isEqualTo(200);
        assertThat(tlsServer.getLastRequest().getHeader("Authorization"))
            .isEqualTo("Bearer synthetic-test-token");
      }
    } finally {
      tlsServer.shutdown();
    }
  }
}

Threat Model

The kubernetes-client sould either fully support SNI (including proper cert validation) or document the current behavior in threat model.

Setting SNI in http clients that do not support it should produce error, not warning. If the warning is to be kept, then this behavior should be documented in threat model.

Other Kubernetes Client Behavior

Other Kubernetes clients that support tls-server-name bind it to certificate identity verification, not just to server certificate selection:

  • client-go copies kubeconfig TLSServerName into Go's tls.Config.ServerName, which Go uses to verify returned certificates and to send SNI for DNS names.
  • The official Python client passes tls-server-name as urllib3 server_hostname; urllib3 verifies TLS against the SNI hostname unless an explicit assert_hostname override is configured.
  • The Kubernetes JavaScript client sets Node TLS servername; Node's default hostname check uses the explicitly configured servername when present.
  • The Kubernetes C# client sets HttpRequestMessage.Headers.Host from TlsServerName; current .NET HttpClient documentation states that a custom Host header is used for SNI and affects certificate validation.
  • The official Java client uses tls-server-name in OkHttp's HostnameVerifier, so it checks the certificate against that value even though it does not appear to configure SNI separately.
  • The Ruby client does not appear to parse tls-server-name, so it is an unsupported-client data point rather than precedent for SNI-only behavior.

This comparison supports the finding: Fabric8's Jetty backend implements only the server-selection half of the field, and the other Fabric8 backends continue after warning that the field is unsupported.

Fix

For backends that cannot enforce tlsServerName as the certificate verification identity, fail closed instead of warning and continuing with URL-host verification.

For Jetty, keep using tlsServerName as SNI and make the Jetty SSLEngine use tlsServerName as the peer host. This preserves Java's default HTTPS endpoint identification during the TLS handshake, so certificate identity is checked against tlsServerName before HTTP headers are sent and before a client certificate can be disclosed during mutual TLS when certificate verification is enabled. Do not fix Jetty by only installing a HostnameVerifier: Jetty invokes that verifier after the handshake succeeds, which is too late to protect client-certificate credentials.


Paweł Płatek from Trail of Bits in collaboration with OpenAI.

@GrosQuildu
GrosQuildu marked this pull request as ready for review June 26, 2026 14:33
@GrosQuildu GrosQuildu changed the title fix: enforce tlsServerName certificate verification fix(httpclient): enforce tlsServerName certificate verification in Jetty, fail loud in other clients Jun 26, 2026
GrosQuildu added a commit to GrosQuildu/kubernetes-client that referenced this pull request Jun 26, 2026
@GrosQuildu
GrosQuildu force-pushed the fix/tls-server-name-cert-verification branch from fdcbe11 to 16ed1a2 Compare June 26, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant