Secrets Kubernetes et vol de tokens de compte de service
Détecter le vol de secrets et de tokens de compte de service Kubernetes dans les logs d'audit : list globaux, rafales, TokenRequest, IP publiques, can-i.
TL;DR. Le vol d'identifiants dans Kubernetes laisse deux types de traces. Côté secrets : un list ou un watch sur secrets sans namespace (tous les secrets du cluster, contenu compris), ou une identité qui lit beaucoup de secrets différents en peu de temps. Côté tokens : un compte de service utilisé depuis une IP publique ou avec kubectl/curl comme user agent, une rafale de kubectl auth can-i, des rafales de 403, et de nouveaux tokens émis via TokenRequest. Chaque secret lu est un identifiant à faire tourner, et sur GKE les lectures ne sont journalisées que si les logs Data Access sont activés.
Les secrets sont la raison pour laquelle la plupart des attaquants s'intéressent à un cluster : mots de passe de bases de données, clés cloud, identifiants de registry, tokens de CI, clés de signature. ATT&CK classe cela sous T1552.007, Container API, et le côté tokens sous T1528, Steal Application Access Token.
Comment les secrets fuient via l'API
Les bonnes pratiques RBAC de Kubernetes insistent sur un point que beaucoup d'équipes ratent : list et watch sur les secrets révèlent leur contenu, exactement comme get. Un rôle qui « ne fait que lister » les secrets est un rôle qui les lit tous.
| Requête | Trace d'audit | Signification |
|---|---|---|
kubectl get secrets -A | list sur secrets, sans objectRef.namespace | Tous les secrets du cluster, en une réponse |
kubectl get secrets -n shop | list sur secrets, namespace shop | Tous les secrets du namespace |
kubectl get secret db-creds -o yaml | get sur secrets, avec un nom | Un secret |
| Créer un pod qui monte un secret | create sur pods avec un volume secret | Lecture indirecte : qui peut créer des pods peut lire les secrets qu'ils montent |
La dernière ligne compte : le droit de créer des workloads dans un namespace donne implicitement accès à ses secrets et à ses tokens de compte de service. Cette lecture n'apparaît pas comme un événement get secrets.
Aux niveaux par défaut d'EKS et de GKE, et dans toute politique raisonnable, les secrets sont journalisés en Metadata : vous voyez qui a lu quel secret, jamais la valeur. C'est suffisant pour cadrer la rotation.
La réserve GKE
Sur GKE, get et list vont dans le log d'audit Data Access, désactivé par défaut (voir le guide d'export). Sans lui, les lectures de secrets ne laissent aucune trace. Sur AKS, la catégorie moins chère kube-audit-admin exclut aussi get et list.
Détections côté secrets
L'analyseur de logs d'audit a deux règles :
- Secrets listés sur tous les namespaces (élevée) :
listouwatchsursecretssans namespace, autorisé, par une identité non système. Les contrôleurs qui surveillent légitimement tous les secrets (par exemple des opérateurs d'ingress ou de certificats) doivent être mis en liste blanche après vérification. - Rafale de lectures de secrets (moyenne) : une identité qui lit 10 secrets différents ou plus en 10 minutes. Compter les noms distincts évite de signaler une application qui relit son propre secret.
Les kubelets (system:node:*) lisent en permanence les secrets des pods planifiés sur leur nœud ; c'est normal et exclu.
Comment les tokens de compte de service sont volés
Les pods reçoivent un token pour leur compte de service, monté par défaut dans /var/run/secrets/kubernetes.io/serviceaccount/token. Depuis Kubernetes 1.22, ce sont des tokens de courte durée, renouvelés automatiquement et obtenus via l'API TokenRequest ; depuis la 1.24, les tokens longue durée stockés dans des Secrets ne sont plus créés automatiquement, selon la documentation des comptes de service. Des Secrets de tokens hérités, créés à la main ou avant une mise à niveau, traînent encore dans beaucoup de clusters.
Chemins de vol typiques :
- lecture du token monté après exploitation d'une application (RCE, SSRF, path traversal) ;
kubectl exec … cat /var/run/secrets/kubernetes.io/serviceaccount/token;- lecture d'un Secret de token hérité, d'un kubeconfig stocké dans un Secret, ou d'une variable de CI ;
- un dashboard ou un outil exposé qui tourne avec un compte de service surprivilégié ;
- émission d'un nouveau token avec
kubectl create token(TokenRequest) quand RBAC autorisecreatesurserviceaccounts/token.
À quoi ressemble un token rejoué
Un token utilisé par son workload suit un schéma stable : IP de pods, user agent propre à l'application, les mêmes quelques appels d'API. Un token rejoué casse ce schéma :
| Signal | Règle de l'analyseur | Sévérité |
|---|---|---|
| Requête d'un compte de service depuis une IP publique (Internet) | Token de compte de service utilisé depuis une IP publique | Élevée |
Compte de service avec un user agent kubectl/, curl/, python-requests/, Go-http-client/… | Token de compte de service utilisé avec kubectl / curl | Moyenne |
5 selfsubjectaccessreviews / selfsubjectrulesreviews ou plus en 5 minutes | Énumération de permissions (kubectl auth can-i) | Moyenne |
| 10 réponses 403 ou plus en 10 minutes | Rafale de requêtes refusées | Moyenne |
| User agent de kube-hunter, peirates, kubeletctl, kubiscan… | User agent d'un outil d'attaque Kubernetes | Élevée |
create sur serviceaccounts/token | Token de compte de service émis | Moyenne |
kubectl auth can-i --list produit une SelfSubjectRulesReview ; kubectl auth can-i create pods une SelfSubjectAccessReview. Les workloads appellent rarement l'une ou l'autre : une rafale depuis un compte de service, c'est quelqu'un qui découvre ce que permet son token volé (ATT&CK T1613).
Réserves à garder en tête :
- Le user agent est contrôlé par le client. Son absence ne prouve rien ; sa présence reste une bonne piste.
- L'IP source provient de la dernière entrée de
sourceIPs. Sur les clusters derrière un proxy ou avec un endpoint privé, une adresse « publique » peut ne jamais apparaître. - Certains opérateurs écrits en Go utilisent un agent générique
Go-http-client. Vérifiez avant d'escalader.
Suivre un token
Les versions récentes de Kubernetes ajoutent un identifiant de credential (le JTI du token) dans user.extra pour les tokens de compte de service. Quand il est présent, il permet de distinguer deux tokens d'un même compte de service : celui du pod et celui que l'attaquant a copié ou émis. Pivotez dessus comme sur une IP.
Pour une TokenRequest, le corps de la requête (journalisé au niveau Request sur EKS et dans la politique recommandée) montre expirationSeconds et les audiences. Un token demandé pour un an (31536000) par un client interactif est une étape de persistance, pas un renouvellement de workload. L'API server peut plafonner la durée de vie des tokens avec --service-account-max-token-expiration.
Délimitation et remédiation
- Listez chaque secret lu par les identités compromises, y compris ceux des réponses de
listglobaux : pour unlistsur tout le cluster, considérez que tous les secrets ont été exposés. - Faites-les tourner à la source : le mot de passe de la base, la clé d'accès cloud, l'identifiant de registry, pas seulement l'objet Secret Kubernetes.
- Invalidez les tokens. Les tokens liés meurent avec l'objet auquel ils sont liés ; les tokens émis via TokenRequest restent valides jusqu'à expiration sauf si vous supprimez le compte de service. Supprimer puis recréer le compte de service, puis redémarrer ses workloads, est la sortie fiable. Supprimez les Secrets de tokens hérités.
- Trouvez le point d'entrée : le pod qui a laissé fuir le token, le dashboard, le job de CI.
- Coupez le chemin :
automountServiceAccountToken: falselà où l'API n'est pas nécessaire, rôles au moindre privilège sanslistsur les secrets, et pas decreatesurserviceaccounts/tokenhors contrôleurs.
Vérifiez aussi le côté cloud : une clé cloud volée dans un Secret s'utilise contre l'API cloud, pas contre Kubernetes. Les sites frères AWS Forensics, GCP Forensics et Azure Forensics couvrent ces logs.