Skip to content

fix(kubernetes-client-api): NO_PROXY hostname boundary matching - #7950

Open
GrosQuildu wants to merge 2 commits into
fabric8io:mainfrom
GrosQuildu:ptp-75-no-proxy-boundary-pr
Open

fix(kubernetes-client-api): NO_PROXY hostname boundary matching#7950
GrosQuildu wants to merge 2 commits into
fabric8io:mainfrom
GrosQuildu:ptp-75-no-proxy-boundary-pr

Conversation

@GrosQuildu

@GrosQuildu GrosQuildu commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

Fabric8 uses the configured NO_PROXY list to decide whether Kubernetes API traffic should bypass the configured proxy. When the Kubernetes API host matches one NO_PROXY entry, HttpClientUtils.configureProxy forces the transport builder to use DIRECT proxy mode.

    String host = master.getHost();
    if (isHostMatchedByNoProxy(host, config.getNoProxy())) {
      builder.proxyType(HttpClient.ProxyType.DIRECT);
    } else {
      builder.proxyAddress(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort()));

However, isHostMatchedByNoProxy treats each non-IP NO_PROXY entry as a raw string suffix. A configuration that excludes corp.example from proxying also excludes evilcorp.example, even though evilcorp.example is neither the exact host corp.example nor a DNS-label subdomain of it.

} else {
  if (host.endsWith(noProxy)) {
    return true;
  }
}

The issue affects all HTTP transports that use HttpClientUtils.applyCommonConfiguration, because the proxy decision happens before the backend-specific builder is returned.

Exploit Scenario

An operator service accepts tenant-supplied cluster registrations but requires all non-corporate Kubernetes API traffic to traverse an internal proxy. The service sets HTTPS_PROXY=http://proxy.internal:8080 and NO_PROXY=corp.example, intending to bypass the proxy only for corp.example and its subdomains. An attacker registers a cluster at https://evilcorp.example. Fabric8 compares the host to corp.example with endsWith, marks the request as direct, and sends Kubernetes client traffic outside the proxy path. The attacker bypasses the egress proxy's allowlist and audit controls, and, if the attacker-controlled endpoint is trusted by the client configuration, can receive credential-bearing Kubernetes API requests.

The following Java regression test can be added to kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java. It uses the real ConfigBuilder path and the real proxy configuration helper. The test fails on the vulnerable implementation because Fabric8 calls proxyType(DIRECT) instead of configuring proxy.internal:8080.

@Test
void testConfigureProxyDoesNotBypassLookalikeNoProxySuffix() throws Exception {
  Config config = new ConfigBuilder()
      .withMasterUrl("https://evilcorp.example.")
      .withHttpsProxy("http://proxy.internal:8080")
      .withNoProxy("corp.example.")
      .build();
  Builder builder = Mockito.mock(HttpClient.Builder.class, Mockito.RETURNS_SELF);

  HttpClientUtils.configureProxy(config, builder);

  Mockito.verify(builder).proxyAddress(new InetSocketAddress("proxy.internal", 8080));
  Mockito.verify(builder).proxyType(HttpClient.ProxyType.HTTP);
  Mockito.verify(builder, Mockito.never()).proxyType(HttpClient.ProxyType.DIRECT);
}

Java PoC demonstrating that NO_PROXY=corp.example. must not match evilcorp.example..

Verification command:

./mvnw -B -ntp -pl kubernetes-client-api \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=HttpClientUtilsTest test

The vulnerable matcher can also be observed directly with the compiled kubernetes-client-api jar:

var clientJar = java.nio.file.Path.of(
    "kubernetes-client/kubernetes-client-api/target/kubernetes-client-api-999-SNAPSHOT.jar")
    .toUri().toURL();
var slf4jJar = java.nio.file.Path.of(
    System.getProperty("user.home")
        + "/.m2/repository/org/slf4j/slf4j-api/2.0.18/slf4j-api-2.0.18.jar")
    .toUri().toURL();
var loader = new java.net.URLClassLoader(new java.net.URL[]{clientJar, slf4jJar});
var cls = Class.forName("io.fabric8.kubernetes.client.utils.HttpClientUtils", true, loader);
var m = cls.getDeclaredMethod("isHostMatchedByNoProxy", String.class, String[].class);
m.setAccessible(true);
System.out.println("evilcorp.example vs corp.example -> "
    + m.invoke(null, "evilcorp.example", new String[]{"corp.example"}));
System.out.println("api.corp.example vs corp.example -> "
    + m.invoke(null, "api.corp.example", new String[]{"corp.example"}));
System.out.println("corp.example vs corp.example -> "
    + m.invoke(null, "corp.example", new String[]{"corp.example"}));

JShell proof using the compiled Fabric8 class.

Observed vulnerable output:

evilcorp.example vs corp.example -> true
api.corp.example vs corp.example -> true
corp.example vs corp.example -> true

The first line is the issue. After the patch, evilcorp.example returns false, while the exact host and subdomain cases still return true.

NO_PROXY Behavior Comparison

Fabric8 documents NO_PROXY as GNU Wget-style proxy configuration, and the maintainer feedback points to the GNU Wget manual as the expected behavior. The key compatibility point is that a hostname entry is not a raw string suffix: corp.example may match corp.example and api.corp.example, but should not match evilcorp.example.

NO_PROXY entry Host Current Fabric8 GNU Wget 1.25.0 curl 8.17.0 / Python urllib Go httpproxy / client-go Proposed patch
corp.example corp.example bypass bypass bypass bypass bypass
corp.example api.corp.example bypass bypass bypass bypass bypass
corp.example evilcorp.example bypass proxy proxy proxy proxy
.corp.example corp.example proxy proxy bypass proxy bypass
.corp.example api.corp.example bypass bypass bypass bypass bypass

The vulnerability is the third row. Current Fabric8 treats evilcorp.example as covered by corp.example, while GNU Wget, curl, Python, Go, and Kubernetes client-go do not.

There is one compatibility nuance for leading-dot entries. The patch below strips a leading dot from hostname entries, so it follows curl/Python behavior for .corp.example: the entry matches both the exact host and subdomains. GNU Wget and Go treat a leading-dot entry as subdomain-only. If maintainers want strict GNU Wget/Go behavior, preserve a leadingDot flag and make .corp.example match api.corp.example but not corp.example. That policy choice is separate from the security fix; both variants must reject evilcorp.example for NO_PROXY=corp.example.

References:

Threat Model

This seems like just a technical bug.

Deduplication

Adjacent public items exist, but they do not fix this issue:

Fix

Require hostname NO_PROXY entries to match either the exact normalized host or a dot-delimited subdomain. Preserve IP and CIDR behavior, normalize case, and strip one trailing DNS root dot from both sides so fully qualified example.com. notation continues to work. The patch below also strips a leading dot from hostname entries, which follows curl/Python behavior; for strict GNU Wget/Go behavior, keep a leading-dot flag and make leading-dot entries match subdomains only.


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

@GrosQuildu
GrosQuildu marked this pull request as ready for review June 26, 2026 13:54
@GrosQuildu GrosQuildu changed the title Fix NO_PROXY hostname boundary matching fix(kubernetes-client-api): NO_PROXY hostname boundary matching Jun 26, 2026
GrosQuildu added a commit to GrosQuildu/kubernetes-client that referenced this pull request Jun 26, 2026
@GrosQuildu
GrosQuildu force-pushed the ptp-75-no-proxy-boundary-pr branch from ab413a1 to 2cf4773 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