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:
kubectl and this client disagree on the same kubeconfig file. kubectl honours proxy-url; the sync Python client does not.
- 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.ProxyURL → rest.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
What happened (please include outputs or screenshots):
The
proxy-urlfield of a kubeconfigclusterentry (kubeconfig v1 API reference) is silently ignored by the synchronous client.load_kube_config()never reads it, soclient.Configuration.proxystaysNoneand 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:
kubectland this client disagree on the same kubeconfig file.kubectlhonoursproxy-url; the sync Python client does not.kubernetes/aio/config/kube_config.pyreadsproxy-urland copies it toConfiguration.proxy, butkubernetes/base/config/kube_config.py— the sync path used byload_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()readsclusters[].cluster.proxy-urland setsConfiguration.proxy, matchingkubectl/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
kindcluster, plus a minimal HTTPCONNECTproxy container attached to thekinddocker network and published on127.0.0.1:18888. The kubeconfigserveris rewritten to the control-plane container's DNS name, which is resolvable inside the docker network but not from the host:kubeadm already puts that name in the API server certificate SANs (
DNS:proxytest-control-plane), so TLS validates normally, with noinsecure-skip-tls-verifyneeded. The only way to reach the API server from the host is through the proxy.Reference behaviour —
kubectl(v1.36.1), same kubeconfig:Proxy log confirms it was used:
Removing only the
proxy-urlline makeskubectlfail, which confirms the proxy is genuinely the sole route:Python client, same kubeconfig, no proxy env vars set:
cfg.proxyisNoneand 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:
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:
Result summary, all against the same cluster and kubeconfig:
Configuration.proxyafter loadkubectlload_kube_config()Nonecfg.proxy = ...kubernetes.aioload_kube_config()proxy-urlRoot cause
KubeConfigLoader._load_cluster_info()handlesserver,certificate-authority,insecure-skip-tls-verifyandtls-server-name, but has no branch forproxy-url, and_set_config()does not copyproxy:The async loader already has exactly the missing two pieces, at
kubernetes/aio/config/kube_config.py#L409-L410and#L418-L419.Secondary finding:
HTTPS_PROXY/HTTP_PROXYare also ignored on theload_kube_config()pathThis 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 togetproxies(), but keys the lookup on the scheme ofself.host:load_kube_config()constructs the object withtype.__call__(Configuration)and nohost(kube_config.py#L778-L781);hostis only assigned afterwards by the loader. At construction timeurlparse("").schemeis'', so neitherhttpnorhttpsever matches:Verified end-to-end against the cluster above: with
HTTPS_PROXYset, or withHTTP_PROXYset,load_kube_config()yieldsproxy=Noneand the call fails. OnlyALL_PROXYworks, because it hits theproxies.get("all")fallback. So today the sole reliable way to put the sync client behind a proxy is to assignConfiguration.proxymanually after loading.(Adjacent, already open: #2520 on
no_proxyenv handling.)Suggested fix
Mirror the async loader — two lines, no new dependency, no behaviour change when
proxy-urlis absent:Optionally, passing the resolved
hostinto theConfigurationconstructor (or re-evaluating the env fallback afterhostis set) would fix theHTTPS_PROXY/HTTP_PROXYcase described above. Happy to open a PR for theproxy-urlpart, 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:
k8s-triage-robotafter stale/rotten; assigned reviewer never responded in ~18 months./lgtm, and the PR still carries thelgtmlabel — but the OWNERS approver never gave final/approve, and the triage bot closed it as rotten. Users asked for a reopen afterwards; none happened.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
/lgtmin #2182, and that the async loader in this repo now implements exactly that logic, the sync change looks uncontroversial and small.Downstream impact
proxy-urlis present, and users must setK8S_AUTH_PROXYby hand, because the underlying Python client drops the field.Comparison with other official clients
proxy-urlhonoured?Cluster.ProxyURL→rest.Config.Proxyintools/clientcmd/client_config.goCluster.proxyUrlinsrc/config.ts, with test fixtureskubernetes.aio)Anything else we need to know?:
The
insecure-skip-tls-verifyroute was deliberately avoided in the reproduction so that this cannot be mistaken for a TLS problem; the certificate validates normally through the proxy.Environment:
kubectl version): client v1.36.1, server v1.36.1 (kind v0.32.0)36.0.0+snapshot,870e9a5acd67e2522018d5c91f4a67b1fbdbb654). On both,_load_cluster_infocontains noproxy-urlhandling and_set_configdoes not copyproxy./kind bug
/kind feature