What happened?
After upgrading the kubernetes Python client from 35.0.0 to 36.0.0, pod exec via kubernetes.stream.stream() consistently fails against an Azure AKS cluster with 401 Unauthorized during the WebSocket handshake. Regular REST API calls (v1.list_namespaced_pod, v1.read_namespaced_pod, etc.) continue to work with the same kubeconfig and same identity. Downgrading the library to 35.0.0 immediately restores exec functionality with no other changes.
What did you expect to happen?
stream(v1.connect_get_namespaced_pod_exec, ...) should succeed in v36.0.0 the same way it does in v35.0.0, against the same AKS cluster, with the same kubeconfig.
How can we reproduce it (as minimally and precisely as possible)?
- AKS cluster (Private Link),
kubectl exec works fine from the same host.
- kubeconfig uses an
exec: auth plugin (kubelogin with Azure AD).
- Run the following Python script with
kubernetes==36.0.0:
from kubernetes import client, config
from kubernetes.stream import stream
cfg = client.Configuration()
cfg.verify_ssl = False
config.load_kube_config(config_file="/path/to/aks/kubeconfig", client_configuration=cfg)
cfg.proxy = None
v1 = client.CoreV1Api(api_client=client.ApiClient(cfg))
# REST: works in both v35 and v36
print([p.metadata.name for p in v1.list_namespaced_pod("vault").items])
# Exec WebSocket: works in v35, fails in v36 with 401
resp = stream(
v1.connect_get_namespaced_pod_exec,
"hashicorp-vault-0", "vault",
command=["/bin/sh", "-c", "vault status"],
stderr=True, stdin=False, stdout=True, tty=False,
)
print(resp)
Anything else we need to know?
The traceback from v36.0.0:
File ".../kubernetes/stream/ws_client.py", line 552, in create_websocket
websocket.connect(url, **connect_opt)
File ".../websocket/_handshake.py", line 158, in _get_resp_headers
raise WebSocketBadStatusException(
websocket._exceptions.WebSocketBadStatusException: Handshake status 401 Unauthorized -+-+-
{'audit-id': '...', 'cache-control': 'no-cache, private', 'content-type': 'application/json',
'date': '...', 'content-length': '129', 'connection': 'close'}
-+-+- b'{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure",
"message":"Unauthorized","reason":"Unauthorized","code":401}\n'
The 401 response carries an audit-id, confirming the request reaches the AKS API server and is rejected at the auth layer (not a network/proxy issue).
Root cause (suspected)
PR #2486 (implement pod exec websockets v5) changed the advertised WebSocket subprotocols from a single value to a comma-separated list:
kubernetes/base/stream/ws_client.py — v35.0.0:
header.append("sec-websocket-protocol: v4.channel.k8s.io")
kubernetes/base/stream/ws_client.py — v36.0.0:
header.append("sec-websocket-protocol: %s,%s" % (V5_CHANNEL_PROTOCOL, V4_CHANNEL_PROTOCOL))
# → "sec-websocket-protocol: v5.channel.k8s.io,v4.channel.k8s.io"
The Authorization header handling is identical between the two versions, and the Authorization header is sent. However, when the AKS API server receives multiple subprotocols in Sec-WebSocket-Protocol, it appears to take a different auth code path (presumably the browser-style base64url.bearer.authorization.k8s.io.<token> lookup inside the subprotocol header) and fails to fall back to honoring the standard Authorization header, returning 401 instead of negotiating down to v4.channel.k8s.io.
This makes the regression appear specifically against cluster/control-plane versions that don't natively support the v5 subprotocol.
Workaround
Pinning kubernetes==35.0.0 resolves the issue.
Kubernetes client version
$ pip show kubernetes | grep Version
Version: 36.0.0
Server version (AKS)
*.privatelink.<region>.azmk8s.io (Azure AKS, control plane managed by Azure; happy to share exact Kubernetes minor if needed)
OS
Red Hat Enterprise Linux 9.x (containerized Python 3.12 environment)
Python version
Python client version
kubernetes==36.0.0
websocket-client==1.9.0
Installation method
Related
What happened?
After upgrading the
kubernetesPython client from35.0.0to36.0.0, pod exec viakubernetes.stream.stream()consistently fails against an Azure AKS cluster with401 Unauthorizedduring the WebSocket handshake. Regular REST API calls (v1.list_namespaced_pod,v1.read_namespaced_pod, etc.) continue to work with the same kubeconfig and same identity. Downgrading the library to35.0.0immediately restores exec functionality with no other changes.What did you expect to happen?
stream(v1.connect_get_namespaced_pod_exec, ...)should succeed in v36.0.0 the same way it does in v35.0.0, against the same AKS cluster, with the same kubeconfig.How can we reproduce it (as minimally and precisely as possible)?
kubectl execworks fine from the same host.exec:auth plugin (kubeloginwith Azure AD).kubernetes==36.0.0:Anything else we need to know?
The traceback from v36.0.0:
The 401 response carries an
audit-id, confirming the request reaches the AKS API server and is rejected at the auth layer (not a network/proxy issue).Root cause (suspected)
PR #2486 (
implement pod exec websockets v5) changed the advertised WebSocket subprotocols from a single value to a comma-separated list:kubernetes/base/stream/ws_client.py— v35.0.0:kubernetes/base/stream/ws_client.py— v36.0.0:The
Authorizationheader handling is identical between the two versions, and theAuthorizationheader is sent. However, when the AKS API server receives multiple subprotocols inSec-WebSocket-Protocol, it appears to take a different auth code path (presumably the browser-stylebase64url.bearer.authorization.k8s.io.<token>lookup inside the subprotocol header) and fails to fall back to honoring the standardAuthorizationheader, returning 401 instead of negotiating down tov4.channel.k8s.io.This makes the regression appear specifically against cluster/control-plane versions that don't natively support the v5 subprotocol.
Workaround
Pinning
kubernetes==35.0.0resolves the issue.Kubernetes client version
Server version (AKS)
*.privatelink.<region>.azmk8s.io(Azure AKS, control plane managed by Azure; happy to share exact Kubernetes minor if needed)OS
Red Hat Enterprise Linux 9.x (containerized Python 3.12 environment)
Python version
Python 3.12.11Python client version
Installation method
Related