Aller au contenu

Jeton de compte de service Kubernetes monté par défaut

Jeton de compte de service Kubernetes monté par défaut

Section intitulée « Jeton de compte de service Kubernetes monté par défaut »

Un conteneur compromis dans Kubernetes n’est pas un incident isolé tant qu’il porte, monté par défaut à un emplacement connu, un jeton d’identité qui parle à l’API du cluster. L’attaquant lit ce jeton, interroge l’API, énumère ce qu’il peut atteindre, et pivote. Cette page monte un cluster de laboratoire, exécute le pivot depuis un pod volontairement vulnérable, puis referme la faille sur ce montage automatique et sur les droits qu’il ouvre.

→ Lire l’explication complète de la marmotte

Pourquoi chaque pod reçoit un jeton d’identité monté par défaut

Section intitulée « Pourquoi chaque pod reçoit un jeton d’identité monté par défaut »

Dans Kubernetes, un pod (la plus petite unité déployable, un ou plusieurs conteneurs partageant le réseau et le stockage) s’exécute sous une identité appelée compte de service (service account). Cette identité sert à parler à l’API server, le point d’entrée central qui pilote tout le cluster. Pour qu’un pod puisse s’adresser à l’API, Kubernetes lui fournit un jeton, un jeton web JSON (JWT, JSON Web Token) signé par le cluster, qui prouve « je suis tel compte de service dans tel espace de noms ».

Le confort a un prix : par défaut, ce jeton est monté automatiquement dans chaque pod, à un emplacement fixe et documenté du système de fichiers du conteneur, /var/run/secrets/kubernetes.io/serviceaccount/token. La grande majorité des pods n’a pourtant jamais besoin de parler à l’API : une application web, un cache, une file de messages tournent parfaitement sans interroger le cluster. Le jeton est donc là, prêt à l’emploi, pour rien. Vingt ans d’automatisation apprennent que le réglage par défaut qui fait gagner cinq minutes au déploiement finit par équiper chaque conteneur d’un trousseau dont personne ne se sert, sauf l’attaquant qui prend la main dessus.

Le point à comprendre est que ce jeton ne crée aucun privilège en lui-même : il présente l’identité du compte de service, et ce que cette identité peut faire dépend entièrement des règles de contrôle d’accès basé sur les rôles (RBAC, Role-Based Access Control). Un jeton attaché à un compte sans aucun droit interroge l’API et se fait tout refuser. Un jeton attaché à un compte lié à un rôle large (lecture des secrets, création de pods, accès à tous les espaces de noms) devient une clé de pivot. Le montage par défaut fournit la clé ; le RBAC décide de ce qu’elle ouvre, et c’est là que se joue la gravité.

Monter l’ouvrage : extraire le jeton et interroger l’API

Section intitulée « Monter l’ouvrage : extraire le jeton et interroger l’API »

Le décor tient en un cluster de laboratoire jetable (un kind ou minikube monté pour l’occasion) avec un pod exécutant une application volontairement vulnérable, sous un compte de service auquel on a laissé, par négligence typique, des droits de lecture trop larges. L’attaquant a obtenu une exécution de commande dans le conteneur (peu importe la faille applicative initiale) et cherche à sortir du pod.

Fenêtre de terminal
# DANS le conteneur compromis, cluster de laboratoire isolé.
# Le jeton et le certificat de l'autorité sont montés au chemin standard,
# par défaut, sans que l'application les ait jamais demandés.
CHEMIN=/var/run/secrets/kubernetes.io/serviceaccount
JETON=$(cat $CHEMIN/token)
ESPACE=$(cat $CHEMIN/namespace)
CA=$CHEMIN/ca.crt
API=https://kubernetes.default.svc # nom DNS interne de l'API server
# 1. À quoi ce jeton a-t-il droit ? SelfSubjectAccessReview répond sans
# déclencher d'alerte : on interroge ses propres permissions.
curl -s --cacert $CA -H "Authorization: Bearer $JETON" \
$API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \
-X POST -H 'Content-Type: application/json' \
-d "{\"spec\":{\"namespace\":\"$ESPACE\"}}"
# 2. Si la lecture des secrets est autorisée, on l'exploite : les secrets
# contiennent souvent d'autres identifiants (base de données, API tierces).
curl -s --cacert $CA -H "Authorization: Bearer $JETON" \
$API/api/v1/namespaces/$ESPACE/secrets

Si le compte de service dispose de la lecture des secrets, l’attaquant récupère en clair (les secrets Kubernetes sont encodés en Base64, pas chiffrés au repos par défaut) les identifiants de bases de données et d’API tierces qui y dorment. Le pivot est enclenché : depuis un conteneur applicatif banal, l’attaquant tient maintenant des accès à d’autres systèmes. Un compte autorisé à créer des pods irait plus loin, en démarrant un pod privilégié monté sur le système de fichiers du nœud.

Montage désactivé et RBAC minimal strictement scopé

Section intitulée « Montage désactivé et RBAC minimal strictement scopé »

Deux contre-mesures, dans l’ordre où elles ferment la faille.

Désactiver le montage automatique du jeton retire la clé des pods qui n’en ont pas besoin, c’est-à-dire la plupart. Le réglage automountServiceAccountToken: false se pose sur le compte de service ou directement sur le pod ; posé sur le pod, il l’emporte. Un conteneur compromis ne trouve alors rien à /var/run/secrets/kubernetes.io/serviceaccount/, et le pivot par l’API s’arrête avant de commencer.

# Compte de service dédié à une appli qui n'interroge PAS l'API :
# le montage automatique du jeton est coupé. Un conteneur compromis
# ne dispose d'aucun jeton à extraire.
apiVersion: v1
kind: ServiceAccount
metadata:
name: front-web
namespace: boutique
automountServiceAccountToken: false # ← la clé n'est plus posée d'avance

Pour les pods qui doivent réellement parler à l’API, le RBAC minimal strictement scopé limite ce que le jeton ouvre, même volé. La règle est de n’accorder que les verbes (get, list, watch) sur les ressources précises nécessaires, dans le seul espace de noms concerné, via un Role (limité à un espace de noms) plutôt qu’un ClusterRole (portée cluster). Éviter les jokers : un resources: ["*"] ou verbs: ["*"] transforme n’importe quelle compromission de pod en compromission de l’espace de noms. La lecture des secrets ne s’accorde qu’aux comptes qui en dépendent, jamais par défaut.

Le compromis central est là : le montage automatique fait gagner du temps au déploiement et évite de câbler l’accès à l’API pod par pod, mais il équipe d’un jeton exploitable des conteneurs qui n’en feront jamais rien. Trancher est net : couper le montage par défaut, ne le rétablir que pour les pods qui interrogent l’API, et leur donner un rôle scopé au strict nécessaire. Ce que ce jeton ne garantit jamais, c’est le chiffrement des secrets au repos : cela vient d’ailleurs (un fournisseur de chiffrement EncryptionConfiguration ou un coffre externe). La même logique d’identité de charge de travail réduite au minimum se retrouve dans la section Failles et dans les principes d’architecture cybersécurité, où un rôle omnibus attaché à une machine est toujours le maillon qui transforme un incident local en compromission d’ensemble.

Sans montage désactivé ni RBAC scopé, tout conteneur compromis dispose d’emblée d’une identité valide vers l’API du cluster, à un chemin fixe et documenté : la compromission d’une application banale devient une reconnaissance du cluster, puis un pivot vers les secrets et les autres charges de travail, sans qu’aucune faille supplémentaire soit nécessaire. La lecture des secrets Kubernetes livre en clair les identifiants qui y dorment, ceux-ci n’étant qu’encodés en Base64 et non chiffrés au repos par défaut. La détection reste faible : les requetes à l’API portent l’identité légitime du compte de service et ressemblent au trafic normal du pod, tandis qu’une revue de type SelfSubjectAccessReview n’apparaît pas comme une action offensive dans les journaux d’audit. Un droit verbs: ["*"] accordé par confort transforme en outre chaque incident de pod en compromission de tout l’espace de noms.

Kubernetes est un chef d’orchestre qui fait tourner des applications dans des boîtes appelées conteneurs, regroupées en unités nommées pods. Pour piloter tout cela, il possède un guichet central : chaque application qui veut demander quelque chose au cluster passe par ce guichet. Pour prouver qui elle est au guichet, une application présente un jeton, une sorte de carte d’identité signée par le cluster.

Le souci vient d’un réglage par défaut. Kubernetes glisse automatiquement ce jeton dans chaque pod, toujours au même endroit du système de fichiers, même quand l’application qui y tourne ne parlera jamais au guichet. Le jeton est donc posé d’avance, prêt, dans presque tous les conteneurs, sans raison.

Si un attaquant prend le contrôle d’un conteneur par une autre faille, il trouve ce jeton sans avoir à le chercher. Il l’utilise pour demander au guichet : « qu’est-ce que j’ai le droit de faire ? ». La réponse dépend des règles de droits attachées à l’identité du jeton. Si ces règles sont trop larges, il peut par exemple lire les secrets du cluster, qui contiennent souvent d’autres mots de passe, et rebondir vers d’autres systèmes.

Pour refermer la faille, on cesse de poser le jeton d’avance dans les conteneurs qui n’en ont pas besoin : l’attaquant ne trouve alors rien à voler. Et pour les rares applications qui doivent parler au guichet, on limite leurs droits au strict minimum, si bien qu’un jeton volé n’ouvre presque rien.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.