Skip to content

kubeconfig proxy-url is silently ignored by the sync client (load_kube_config), while kubernetes.aio honors it #2679

Description

@yurnov

What happened (please include outputs or screenshots):

The proxy-url field of a kubeconfig cluster entry (kubeconfig v1 API reference) is silently ignored by the synchronous client. load_kube_config() never reads it, so client.Configuration.proxy stays None and all API traffic is attempted directly, bypassing the proxy the user explicitly configured.

There is no warning and no error about the ignored field — the connection just fails if the API server is only reachable through the proxy, or (worse, when the proxy exists for egress-policy reasons) it silently egresses directly.

Two things make this more than a plain feature gap:

  1. kubectl and this client disagree on the same kubeconfig file. kubectl honours proxy-url; the sync Python client does not.
  2. The async client in this very repository already implements it. kubernetes/aio/config/kube_config.py reads proxy-url and copies it to Configuration.proxy, but kubernetes/base/config/kube_config.py — the sync path used by load_kube_config() — does not. So the two clients shipped from this repo behave differently on the same file.

What you expected to happen:

load_kube_config() reads clusters[].cluster.proxy-url and sets Configuration.proxy, matching kubectl/client-go and matching this repo's own async client.

How to reproduce it (as minimally and precisely as possible):

I reproduced this against a live cluster, with the API server reachable only through a proxy, so that "was the proxy used?" is unambiguous rather than inferred from logs.

Setup: a kind cluster, plus a minimal HTTP CONNECT proxy container attached to the kind docker network and published on 127.0.0.1:18888. The kubeconfig server is rewritten to the control-plane container's DNS name, which is resolvable inside the docker network but not from the host:

$ getent hosts proxytest-control-plane      # not resolvable from the host
$ echo $?
2

kubeadm already puts that name in the API server certificate SANs (DNS:proxytest-control-plane), so TLS validates normally, with no insecure-skip-tls-verify needed. The only way to reach the API server from the host is through the proxy.

clusters:
- cluster:
    certificate-authority-data: <from kind>
    server: https://proxytest-control-plane:6443
    proxy-url: http://127.0.0.1:18888
  name: kind-proxytest

Reference behaviour — kubectl (v1.36.1), same kubeconfig:

$ kubectl --kubeconfig kc-proxy.yaml get nodes
NAME                      STATUS   ROLES           AGE     VERSION
proxytest-control-plane   Ready    control-plane   2m22s   v1.36.1

Proxy log confirms it was used:

PROXY-LOG CONNECT proxytest-control-plane:6443

Removing only the proxy-url line makes kubectl fail, which confirms the proxy is genuinely the sole route:

$ kubectl --kubeconfig kc-noproxy.yaml get nodes
Unable to connect to the server: dial tcp: lookup proxytest-control-plane on 10.255.255.254:53: no such host

Python client, same kubeconfig, no proxy env vars set:

from kubernetes import client, config

config.load_kube_config(config_file="kc-proxy.yaml")
cfg = client.Configuration.get_default_copy()
print("cfg.host :", cfg.host)
print("cfg.proxy:", repr(cfg.proxy))
client.CoreV1Api().list_node(_request_timeout=15)
cfg.host : https://proxytest-control-plane:6443
cfg.proxy: None
RESULT   : FAILED - MaxRetryError: HTTPSConnectionPool(host='proxytest-control-plane', port=6443):
           Max retries exceeded with url: /api/v1/nodes (Caused by NameResolutionError(...))

cfg.proxy is None and nothing is logged through the proxy — the field was parsed away and dropped.

The transport itself is fine. Assigning the identical value by hand works immediately:

cfg = client.Configuration()
config.load_kube_config(config_file="kc-proxy.yaml", client_configuration=cfg)
print(repr(cfg.proxy))          # None
cfg.proxy = "http://127.0.0.1:18888"   # the line every user has to add today
client.CoreV1Api(client.ApiClient(cfg)).list_node(_request_timeout=15)
# RESULT : SUCCESS - ['proxytest-control-plane']

So this is purely a kubeconfig-parsing gap, not a urllib3/transport limitation. The missing piece is one assignment in the loader.

The async client in this repo, same kubeconfig, same moment:

from kubernetes.aio import client as aio_client
from kubernetes.aio.config import load_kube_config

cfg = aio_client.Configuration()
await load_kube_config(config_file="kc-proxy.yaml", client_configuration=cfg)
print(cfg.host, repr(cfg.proxy))
# https://proxytest-control-plane:6443 'http://127.0.0.1:18888'

Result summary, all against the same cluster and kubeconfig:

Path Configuration.proxy after load API call
kubectl n/a ✅ works via proxy
sync load_kube_config() None ❌ fails
sync + manual cfg.proxy = ... set ✅ works via proxy
async kubernetes.aio load_kube_config() set from proxy-url

Root cause

KubeConfigLoader._load_cluster_info() handles server, certificate-authority, insecure-skip-tls-verify and tls-server-name, but has no branch for proxy-url, and _set_config() does not copy proxy:

keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl','tls_server_name']

The async loader already has exactly the missing two pieces, at kubernetes/aio/config/kube_config.py#L409-L410 and #L418-L419.

Secondary finding: HTTPS_PROXY / HTTP_PROXY are also ignored on the load_kube_config() path

This matters because "just use the env vars" is the workaround usually suggested in the earlier threads, and on current master it does not work for the two most common variables.

Configuration.__init__ falls back to getproxies(), but keys the lookup on the scheme of self.host:

scheme = urlparse(self.host).scheme
proxy = proxies.get(scheme) or proxies.get("all")

load_kube_config() constructs the object with type.__call__(Configuration) and no host (kube_config.py#L778-L781); host is only assigned afterwards by the loader. At construction time urlparse("").scheme is '', so neither http nor https ever matches:

$ HTTPS_PROXY=http://127.0.0.1:18888 python -c "
from kubernetes.client import Configuration
print(repr(Configuration().proxy))
print(repr(Configuration(host='https://x:6443').proxy))"
None
'http://127.0.0.1:18888'

Verified end-to-end against the cluster above: with HTTPS_PROXY set, or with HTTP_PROXY set, load_kube_config() yields proxy=None and the call fails. Only ALL_PROXY works, because it hits the proxies.get("all") fallback. So today the sole reliable way to put the sync client behind a proxy is to assign Configuration.proxy manually after loading.

(Adjacent, already open: #2520 on no_proxy env handling.)

Suggested fix

Mirror the async loader — two lines, no new dependency, no behaviour change when proxy-url is absent:

--- a/kubernetes/base/config/kube_config.py
+++ b/kubernetes/base/config/kube_config.py
@@ def _load_cluster_info(self):
         if 'tls-server-name' in self._cluster:
             self.tls_server_name = self._cluster['tls-server-name']
+        if 'proxy-url' in self._cluster:
+            self.proxy = self._cluster['proxy-url']
 
@@ def _set_config(self, client_configuration):
-        keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl','tls_server_name']
+        keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl',
+                'tls_server_name', 'proxy']

Optionally, passing the resolved host into the Configuration constructor (or re-evaluating the env fallback after host is set) would fix the HTTPS_PROXY/HTTP_PROXY case described above. Happy to open a PR for the proxy-url part, the env-var part, or both — please say which you'd prefer, and whether the env-var half should be a separate issue.

Prior art — this has been reported and implemented before, and closed only by lifecycle automation

Issues, both closed as not planned by the triage bot after going rotten, neither with any maintainer objection:

Pull requests, all closed unmerged:

To the best of my reading of those threads, no maintainer has ever raised a technical objection to the feature itself — every closure was procedural (inactivity lifecycle, or an unrelated generator upgrade bundled into the same PR). Given that a scoped version already got an /lgtm in #2182, and that the async loader in this repo now implements exactly that logic, the sync change looks uncontroversial and small.

Downstream impact

Comparison with other official clients

Client proxy-url honoured?
client-go (reference impl.) Yes — Cluster.ProxyURLrest.Config.Proxy in tools/clientcmd/client_config.go
javascript Yes — Cluster.proxyUrl in src/config.ts, with test fixtures
python (sync) No
python (async, kubernetes.aio) Yes
csharp No (field not parsed at all)

Anything else we need to know?:

The insecure-skip-tls-verify route was deliberately avoided in the reproduction so that this cannot be mistaken for a TLS problem; the certificate validates normally through the proxy.

Environment:

  • Kubernetes version (kubectl version): client v1.36.1, server v1.36.1 (kind v0.32.0)
  • OS: Linux (WSL2, kernel 6.18.33.2)
  • Python version: 3.12.3
  • Python client version: reproduced on released 36.0.3 from PyPI and on master (36.0.0+snapshot, 870e9a5acd67e2522018d5c91f4a67b1fbdbb654). On both, _load_cluster_info contains no proxy-url handling and _set_config does not copy proxy.

/kind bug
/kind feature

Metadata

Metadata

Assignees

Labels

kind/bugCategorizes issue or PR as related to a bug.kind/featureCategorizes issue or PR as related to a new feature.triage/acceptedIndicates an issue or PR is ready to be actively worked on.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions