← Présentation

Audit DevSecOps de deux clusters Kubernetes

Audit d'un cluster EKS managé et d'un cluster kubeadm auto-hébergé, même méthode, mêmes outils, mêmes scénarios.

Version 1.2 Périmètre gelé au 2026-08-18 Version assainie

🟢 VERSION ASSAINIE (décision D8), dérivée de rapport-audit.md par assainir.py. Identifiants de compte, noms de ressources et de dépôts remplacés par des marqueurs stables : <ACCOUNT_ID>, <cluster-eks>, <projet-eks>, <repo-eks>, <repo-homelab>, <bucket-tfstate>, <bot-renovate>, <groupe-gitlab>, <owner>.

<projet-eks> est le préfixe commun des ressources AWS du projet : il apparaît donc dans des noms composés comme <projet-eks>-gitlab-ci, dont le suffixe est conservé pour que la chaîne AW-2 reste lisible.

Ce qui est conservé l'est délibérément : les adresses 192.168.1.x (RFC1918, non routables, et §5.4bis démontre la non-exposition du homelab en montrant qu'il répond sur une adresse privée), ainsi que les noms de rôles iamadmin, gitlab-ci et prowler-audit, sans lesquels la chaîne AW-2 ne se lit plus.

Ne pas éditer ce fichier : il se régénère. Toute correction va dans la version complète.

Audit DevSecOps de deux clusters Kubernetes

Cluster EKS managé (AWS) et cluster kubeadm auto-hébergé (homelab)

Version 1.2, version publiée. Ajout de la relecture OWASP (§3.11) le 2026-09-08. La 1.1 du 2026-08-27 ajoutait EK-17 et l'action 38, sur un signal du fournisseur postérieur au gel
Auteur Youssef Kadi
Date de rédaction 2026-08-19 au 2026-08-24, relecture close le 2026-08-26
Date de gel du périmètre 2026-08-18 pour les dépôts, chaque mesure en vol porte sa propre date
Statut 🟢 Publié le 2026-09-08. La diffusion publique était conditionnée à la fermeture des deux constats critiques AW-2 et AW-9 (§1.3.9), et non à une date. La condition est remplie : AW-9 fermé le 2026-08-27, AW-2 le 2026-09-06. Les deux restent décrits ici tels qu'ils ont été mesurés au gel du périmètre, un rapport d'audit ne se réécrit pas au fil des corrections

Table des matières

Section Titre Ce qu'on y trouve
0 Synthèse exécutive Le verdict en trois clauses et les six risques majeurs. Écrite pour se lire seule : si vous n'avez que cinq minutes, c'est ici
1 Périmètre et méthode Ce qui est couvert et surtout ce qui ne l'est pas, les quatre référentiels, et six règles de lecture des chiffres qui ont écarté des constats faux
2 Architecture auditée Les deux clusters décrits et schématisés, 5 vues : 3 pour le homelab, 2 pour EKS
3 Constats par domaine 48 constats : 20 sur le homelab, 17 sur EKS, 11 sur le compte AWS. Chacun porte sa preuve, son impact et sa recommandation. Se termine par la relecture OWASP (§3.11), qui range les 48 sous les dix familles et nomme ce qu'aucune ne couvre
4 Purple team 10 scénarios d'attaque tirés de MITRE ATT&CK, joués sur les deux clusters. Trois questions à chaque fois : bloqué, détecté, tracé
5 Comparatif managé / auto-hébergé 6 axes mesurés des deux côtés avec la même méthode et les mêmes outils. C'est l'apport propre de ce rapport
6 Plan de remédiation 38 actions classées par risque supprimé rapporté à l'effort, plus 3 de clôture de l'audit lui-même
7 Annexes Les commandes rejouables à tag épinglé, les douze points de méthode, et les sorties brutes

Les nombres de ce tableau sont vérifiés à chaque génération par assainir.py, qui les recompte dans le document. Un constat ajouté sans mise à jour de cette ligne fait échouer la vérification, au lieu de laisser dériver le sommaire.


0. Synthèse exécutive

Le verdict en une phrase

Les deux clusters bloquent une partie des attaques, aucun ne les reconnaît comme des attaques, et seul le cluster managé en garde la trace.

Chaque scénario a été jugé sur ces trois questions, et sur elles seules : est-il bloqué ? est-il détecté ? en reste-t-il une trace ?

Cluster managé (EKS) Cluster auto-hébergé (kubeadm)
Bloqué ? 🟠 5 sur 9 🟠 2 sur 9
Détecté ? 🔴 aucune détection d'attaque, 2 signaux partiels 🔴 aucune détection d'attaque, 1 signal partiel
Tracé ? 🟢 9 sur 9, et la trace survit à la destruction du cluster 🔴 aucune, 0 sur 9

La ligne « Tracé » est l'écart le plus net de toute la campagne, et c'est le seul endroit où les deux architectures ne se valent pas du tout.

⚠️ « Aucune détection » ne veut pas dire « aucun signal ». Trois scénarios ont produit quelque chose : un Warning d'admission sur les deux clusters (PT-5), et sur EKS l'appel d'API journalisé sans son exécution (PT-7). Aucun n'est une alerte, aucun ne nomme une attaque, aucun n'aurait réveillé qui que ce soit. C'est la raison pour laquelle ils comptent comme non détectés, et non parce que rien n'aurait été émis.

📐 Les dénominateurs. Dix scénarios ont été définis, mais tous n'ont pas de vecteur sur les deux clusters : PT-6 (métadonnées d'instance) est sans objet hors cloud, donc absent du homelab, et PT-2 (secrets au repos) n'a aucun vecteur sur EKS, faute de chiffrement à configurer (EK-2). Neuf scénarios mesurables de chaque côté, dix-neuf exécutions au total. Un scénario sans vecteur n'est pas un échec de trace : c'est un événement qui n'a pas eu lieu.

Ce n'est pas une impression, c'est une mesure, rejouée sur les deux clusters. Le blocage reste partiel des deux côtés — il tient là où un contrôle a été posé, pas ailleurs — mais il est net partout où il existe, et il est un cran meilleur sur EKS : là où le homelab authentifie encore une requête sans jeton comme system:anonymous avant de la refuser, EKS refuse l'authentification elle-même (PT-1). En face, aucun des dix-neuf scénarios joués n'a été détecté en tant qu'attaque, sur aucun des deux clusters. Mais la trace diverge franchement : sur le homelab, aucun des neuf scénarios joués ne laisse d'enregistrement consultable après coup, seul un objet résiduel subsistant sans que rien ne le signale (PT-8, §4.3). Sur EKS, neuf scénarios sur dix laissent une trace qui survit à la destruction du cluster — seul reste invisible le contenu de ce qui a été exécuté, pas le fait que l'appel a eu lieu.

Ce qui rend ce constat solide

Deux architectures opposées ont été soumises à la même campagne. Un cluster managé chez AWS et un cluster kubeadm monté à la main n'ont presque rien en commun. Ils ont pourtant le même trou : la détection à l'exécution. Zéro scénario détecté, sur dix-neuf joués, deux référentiels de contrôle différents.

Ce que la journée live EKS a changé, ce n'est pas ce trou : c'est la preuve qu'il ne se referme pas en changeant d'infrastructure. Le managé déplace la trace (le journal d'audit de l'API survit à tout, y compris à la destruction du cluster qui l'a produite), il ne déplace pas la détection. Un journal qui capture qu'un kubectl exec a eu lieu, sans jamais savoir ce qui s'est passé dedans, reste un journal d'accès, pas un outil de détection.

Quand un défaut se répète sur deux infrastructures qui n'ont pas la même origine, ce n'est plus un accident de configuration. C'est une habitude de conception, et c'est une conclusion.

Les six risques majeurs

# Risque Identifiant Criticité
1 La clé d'accès de l'administrateur ouvre le compte entier sans authentification forte. Le MFA existe, mais il n'est exigé que sur la console, et aucune politique ne l'impose sur l'API AW-9 🔴 Critique
2 Une clé de CI compromise donne l'administration du cluster et un chemin d'escalade vers l'administration du compte AWS, sans qu'aucun service ne le détecte AW-2 🔴 Critique
3 ArgoCD est cluster-admin sur le cluster auto-hébergé, sans aucun garde-fou en aval : compromettre le dépôt qu'il synchronise ferme la chaîne jusqu'à root sur les nœuds, chaque maillon mesuré séparément C-U 🔴 Critique
4 Aucune investigation rétroactive n'est possible sur le cluster auto-hébergé : le journal d'audit de l'API n'existe pas C-O 🟠 Élevée
5 L'agent de collecte de logs peut lire tous les secrets du cluster, homelab et EKS, par un défaut de chart jamais décidé — pas une installation isolée C-Q 🟠 Élevée
6 Le cloisonnement par environnement d'EKS (dev / staging / prod) est contourné par le rôle du contrôleur qui distribue les secrets lui-même : une identité de plateforme peut lire un secret de production AW-11 🟠 Élevée

Deux risques écartés du tableau, mais à garder à l'œil :

  • Le cluster auto-hébergé n'est pas restaurable (C-M + C-S, un seul plan de contrôle, aucune sauvegarde d'etcd, aucune limite mémoire). Retiré parce que son impact reste la disponibilité, pas la confidentialité ou l'intégrité comme les six risques ci-dessus — traité en priorité 2 du plan de remédiation (§6.2).
  • Deux consoles d'administration exposées sur Internet côté EKS, dont ArgoCD (EK-9). Retiré au profit de C-U : la même exposition d'ArgoCD existe côté EKS (confirmée RE-5), mais l'authentification applicative a effectivement bloqué l'accès testé en PT-4, alors que C-U décrit une chaîne déjà fermée jusqu'au bout, sans ce rempart. Reste couvert en priorité 2 (§6.2, action 28).

Ce que l'audit a délibérément écarté

Un rapport qui garde tout ce que ses outils produisent n'est pas un audit, c'est un export.

Six familles de résultats ont été rejetées après vérification, chacune avec son argument (section 1.5 et section 3.9). Sans ce tri, ce rapport aurait annoncé un compte racine sans authentification forte, vingt-trois conteneurs tournant en root, et 157 problèmes de sécurité sur le compte cloud, là où les mêmes lignes ne recouvrent que 41 contrôles distincts (§1.5.3).

Les deux premières affirmations sont fausses. La troisième est un chiffre exact lu de travers, ce qui est plus insidieux : il se vérifie, il se cite, et il ne veut pas dire ce qu'il paraît dire.


1. Périmètre et méthode

1.1 Nature de cet audit

C'est un auto-audit, conduit selon la méthode d'un audit externe. L'auteur du rapport est l'auteur des deux infrastructures.

Ce point est écrit ici, en première page, volontairement. Le présenter comme un audit externe serait faux et se verrait en trois questions. Assumé, il devient une information utile au lecteur : il sait quel biais chercher.

Le biais de l'auto-audit est la complaisance. Trois garde-fous ont été posés contre lui :

  1. Les référentiels sont extérieurs. Un contrôle en échec est en échec parce qu'un organisme tiers l'a écrit, pas parce que l'auteur a décidé de le trouver.
  2. La preuve prime sur l'intention. Tout constat tiré d'un dépôt porte la mention déclaratif. Il ne devient une preuve qu'une fois rejoué sur l'infrastructure en marche.
  3. On collecte, on ne répare pas. Pendant les phases de mesure, aucune correction n'est appliquée. Un défaut réparé avant d'être écrit disparaît du rapport, et l'audit paie son temps sans produire son livrable. Une seule exception a été accordée, elle est datée et signalée (constat C-L).

1.2 Périmètre

Deux clusters Kubernetes et le compte AWS qui héberge l'un des deux.

Périmètre Ce que c'est Gel
Cluster EKS Kubernetes v1.32 managé par AWS, région eu-west-3, cluster <cluster-eks>, deux groupes de nœuds. Infrastructure éphémère, montée et détruite à la demande dépôt <repo-eks>, branche develop, commit 5d4e85d
Cluster homelab Kubernetes v1.36.2 monté par kubeadm, trois machines virtuelles sur un hyperviseur Proxmox, un plan de contrôle et deux nœuds de travail dépôt <repo-homelab>, branche main, commit 32d1f44
Compte AWS compte <ACCOUNT_ID>, région principale eu-west-3 scan du 2026-08-17, infrastructure EKS détruite au moment du scan
Poste d'administration la machine qui porte les clés des deux infrastructures mesure du 2026-08-18

Le plan de contrôle est la partie de Kubernetes qui décide : l'API, l'ordonnanceur, la base etcd. Les nœuds de travail exécutent les conteneurs.

1.3 Ce qui n'est pas couvert

Un audit qui ne dit pas ce qu'il n'a pas regardé n'est pas un audit. Six exclusions, toutes assumées.

1.3.1 Le projet platform (Talos + Cluster API)

Hors périmètre. Le projet n'était pas actif à la date de gel. L'inclure aurait consommé la fenêtre de temps sans produire de mesure comparable, faute d'infrastructure en marche.

1.3.2 Le dépôt applicatif App-todos

Seul son déploiement est audité, à travers le namespace todos-demo du homelab. La chaîne qui produit l'image ne l'est pas.

Ce n'est pas une lacune, c'est un exercice différent. Auditer une application se fait sur d'autres référentiels : OWASP ASVS pour le code, la chaîne d'intégration continue, les dépendances. Un audit applicatif dédié est planifié après la livraison de ce rapport.

1.3.3 Le plan de contrôle EKS, structurellement inaccessible

Sur EKS, le plan de contrôle appartient à AWS. Les machines qui l'hébergent ne sont pas joignables.

Conséquence directe : kube-bench ne peut auditer que les nœuds de travail. Toute la section 1 du CIS Kubernetes Benchmark, dont les contrôles 1.2.16 à 1.2.19 sur la journalisation d'audit de l'API, ne sera pas évaluée côté EKS.

🔴 Ce n'est ni un défaut de l'outil ni un trou de cet audit. C'est un constat de la section 5. La liste des contrôles que le homelab évalue et qu'EKS n'évalue pas est, en creux, la liste exacte de ce que le service managé prend en charge à la place du client.

Cette limite est écrite ici, avant le moindre chiffre, parce que sans elle la comparaison des verdicts CIS entre les deux clusters serait fausse.

1.3.4 Le namespace kube-system du homelab

Non géré par le projet, et il ne le sera pas. C'est une décision documentée du projet homelab, pas un oubli. Elle a des conséquences mesurées, portées aux constats C-X (aucun label d'admission) et C-S (16 de ses conteneurs sans limite mémoire).

1.3.5 Le poste d'administration, partiellement

Le poste est mesuré (constats C-G et C-N) mais il n'est pas audité : il n'est couvert par aucun référentiel, aucun scan, aucun rôle de durcissement.

Cette exclusion est elle-même un constat. Les deux clusters ont été audités, la machine qui les administre ne l'a jamais été.

1.3.6 Un tunnel sortant vers Internet, hors du périmètre et non mesuré

Déclaré par l'exploitant le 2026-08-19, en cours d'audit. Écrit ici parce qu'un audit qui apprend l'existence d'une voie d'exposition et ne l'écrit pas vaut moins qu'un audit qui l'écrit sans avoir pu la mesurer.

Quoi un tunnel Cloudflare publiant un service n8n (automatisation de flux)
une machine virtuelle du même hyperviseur, distincte des trois nœuds du cluster
État à la date de gel éteinte — ⚠️ déclaratif, dit par l'exploitant, jamais mesuré indépendamment (aucun scan, aucune requête sortante vérifiée)
Cycle de vie démarré manuellement pour une session de travail, arrêté à la fin — également déclaratif

Ce que ça implique pour la lecture du rapport, et c'est la raison d'être de ce paragraphe.

Un tunnel sortant publie un service sans aucune redirection de port. La connexion part de l'intérieur vers le fournisseur. La table de redirection de la box reste vide, et le service est pourtant joignable depuis Internet.

Une vérification par les règles de la box n'aurait donc rien vu. C'est un cas concret et daté du motif que ce rapport documente ailleurs : le contrôle qu'on croit lire n'est pas celui qui s'applique.

Ce qui n'a pas été mesuré, et ne le sera pas dans cette campagne : les règles d'ingress du tunnel, donc ce vers quoi il route réellement ; la présence ou non d'une authentification en amont ; l'emplacement des identifiants du tunnel ; et l'adresse réseau de la machine virtuelle.

Conséquence assumée sur la conclusion. La section 5.4 bis prouve qu'aucun service du cluster n'est joignable depuis Internet. Elle ne prouve pas, et ne prétend pas, que rien du réseau local ne l'est.

📌 Note de méthode. Cet élément est arrivé par la conversation, pas par la mesure. Le périmètre avait été découpé en « deux clusters, un compte cloud, un poste d'administration ». Une voie d'exposition se trouvait juste à côté de ce découpage, sur le même réseau. Un audit ne découvre que ce que son périmètre lui permet de regarder, et c'est précisément pourquoi la section 1.3 existe.

1.3.7 Le contenu des secrets

Aucune valeur de secret n'a été affichée pendant l'audit. Seuls les noms de clés et les longueurs ont été relevés. Un contrôle porte donc sur la présence d'un secret, jamais sur sa qualité.

Conséquence honnête sur le constat C-E : la vérification a porté sur les noms de clés, pas exhaustivement sur chaque valeur. Le constat s'écrit « aucun identifiant trouvé par examen des noms de clés », pas « il n'y en a aucun ».

1.3.8 Les deux clusters ne sont pas au même âge de projet

C'est la limite de validité de la section 5, et elle doit être lue avant elle.

Les deux infrastructures ont été mesurées avec la même méthode, mais pas au même moment de leur vie. Le cluster EKS sort de six sprints ; le homelab en a clos un seul, et plusieurs chantiers de sécurité y sont planifiés, pas absents.

Chantier Homelab Statut à la date de gel
Pod Security Standards livré le 10/08 (#79, #81) 13 namespaces sur 14, seul kube-system reste (C-X)
NetworkPolicy 🔜 #80, hors milestone séquencé après les PSS, délibérément
Sauvegarde etcd, HA du plan de contrôle 🔜 Sprint 3 Rebuild décidé le 18/08 (C-M)
Alerting 🔜 Sprint 2 01/09 → 28/09

Le séquencement de #80 est une décision, pas un oubli, et sa raison est écrite dans le dépôt : une politique d'admission mal réglée refuse avec un message, une politique réseau mal réglée coupe un flux en silence. Poser la seconde avant la première, c'est se priver du filet.

🔴 Et il faut écrire l'effet le plus gênant, sinon ce rapport se flatte. Le Sprint 2 du homelab a été décalé du 01/09 au 28/09, et le Sprint 3 au 29/10, le 17/08, explicitement pour libérer la fenêtre de cet audit. Cet audit ne se contente donc pas de mesurer un projet en cours de route : il a lui-même repoussé les travaux qu'il constate manquants.

Conséquence sur la lecture de la section 5, et §5.7 fait déjà ce tri. Sa synthèse range chaque axe selon que le résultat relève du fournisseur, du coût d'accès au contrôle, ou du travail fait. Ce paragraphe n'ajoute pas un second classement, il dit ce que ce tri implique :

  • les axes attribués au fournisseur ne bougeraient pas si le homelab était plus mûr. §5.7 n'en retient qu'un au sens strictement technique, l'axe 5.
  • les axes attribués au travail fait se rejoueront, notamment l'axe 3 (cloisonnement réseau), dont l'écart se referme mécaniquement le jour où #80 est livrée. §5.3 le dit déjà pour cet axe ; ce qui manquait, c'est que ce travail est planifié, pas oublié.

🔴 Second décalage, découvert le 27/08 et dans l'autre sens. Les deux clusters ne sont pas non plus à la même version : EKS en 1.32, homelab en v1.36.2, quatre versions mineures d'écart. Cette fois, c'est le cluster managé qui est en retard (EK-17, §3.7).

L'effet sur la section 5 est réel mais borné : aucun des six axes ne repose sur une fonctionnalité apparue entre ces versions, ils portent tous sur des contrôles présents des deux côtés depuis longtemps. Ce que ce décalage change, c'est la lecture de l'ensemble : un lecteur en droit de supposer deux installations comparables doit savoir que l'une est plus ancienne de quatre versions.

Un comparatif entre deux clusters au même âge de projet serait plus équitable, et plus intéressant. Le calendrier ne l'a pas permis. Ce qui est dit ici n'est donc pas « le managé fait mieux », mais « voici où l'écart vient du fournisseur, et où il vient de nous ».

1.3.9 Ce que la version assainie de ce rapport ne protège pas

Mesuré le 2026-08-26. Deux versions de ce rapport existent (décision D8) : une complète, privée, et une assainie où l'identifiant de compte, le nom du cluster, le bucket d'état et les dépôts sont remplacés par des marqueurs.

🔴 Cette substitution ne rend pas la version assainie anonyme, et il faut l'écrire. Les dépôts du projet audité sont publics, et ils publient déjà ce que le rapport masque : l'identifiant de compte apparaît dans 13 fichiers publiés, le bucket d'état dans 6, le nom du cluster dans 8, le domaine dans 66. Un lecteur qui atteint ce rapport atteint aussi les dépôts.

Le domaine a d'ailleurs été retiré du masquage le 2026-09-08, et c'est la conclusion de ce paragraphe appliquée à elle-même. Le domaine est celui du site public de l'auteur et de son adresse professionnelle : le remplacer par un marqueur ne cachait rien à personne, et empêchait le dépôt qui publie ce rapport de citer sa propre adresse. Un masquage qui gêne l'auteur sans gêner un attaquant est un coût pur.

La conséquence est plus générale que ce document. Masquer un identifiant que le lecteur peut retrouver ailleurs ne réduit pas le risque, cela le déguise. Et un constat reste exploitable sans son identifiant : « <ACCOUNT_ID> porte une clé d'administration sans MFA » vaut la version nommée, pour qui résout le marqueur en une recherche.

Ce qui protège réellement n'est donc pas le masquage, c'est l'état des constats. La diffusion publique de ce rapport a donc été conditionnée à la fermeture de ses constats critiques, pas à la présence de marqueurs.

Cette condition est remplie, et c'est ce qui autorise la publication de ce document. AW-9 a été fermé le 2026-08-27, AW-2 le 2026-09-06, chacun avec ses preuves. Jusqu'à ces dates, le rapport n'a circulé que par envoi direct. Les deux constats restent décrits ci-après tels qu'ils ont été mesurés au gel du périmètre : un rapport d'audit consigne un état daté, il ne se réécrit pas au fur et à mesure des corrections.

Le cas d'un audit commandé par un client est différent, et c'est le cas normal du métier. Là, l'assainissement protège un tiers d'un lecteur qui ne le connaît pas, et il fonctionne : l'identité du client n'est publiée nulle part ailleurs, donc le marqueur ne se résout pas. S'y ajoutent des gestes que cet auto-audit n'a pas à poser : mention de confidentialité en première page, diffusion nominative et tracée, durée de conservation convenue, et destruction ou restitution des preuves à l'échéance. Ce rapport-ci est un auto-audit publié par son propre auteur (§1.1) : c'est ce qui rend le masquage inopérant ici, et non une faiblesse de la méthode.

1.4 Les quatre référentiels

Un référentiel est une liste de contrôles écrite par un organisme tiers, contre laquelle on compare une installation réelle. Il apporte trois choses qu'un audit improvisé n'a pas : une liste fermée, donc on sait ce qui n'a pas été regardé ; une autorité extérieure, donc le constat ne dit pas « je trouve que » ; et une comparabilité, donc la section 5 existe.

Référentiel Publié par Question posée Outil Périmètre ici
CIS Kubernetes Benchmark Center for Internet Security ma configuration s'écarte-t-elle de la recommandation ? kube-bench v0.16.0 les deux clusters
CIS AWS Foundations Benchmark Center for Internet Security idem, au niveau du compte cloud prowler 5.39.0 compte AWS
MITRE ATT&CK for Containers MITRE quels chemins d'attaque mon installation laisse-t-elle ouverts ? kubescape-cli v4.0.12 + jeu manuel les deux clusters
NSA/CISA Kubernetes Hardening Guide NSA et CISA mes conteneurs et mon réseau sont-ils durcis ? kubescape-cli v4.0.12 les deux clusters
OWASP Kubernetes Top 10, version 2025 fondation OWASP ai-je un angle mort ? lecture manuelle grille de relecture finale, §3.11

Quatre référentiels parce qu'aucun ne couvre seul la configuration, les chemins d'attaque et les angles morts. Le tableau ci-dessus compte cinq lignes pour quatre référentiels : CIS est appliqué à deux niveaux, le cluster et le compte cloud, avec deux outils différents.

⚠️ La version 2025 de la grille OWASP remplace la version 2022, encore la plus citée, et huit codes sur dix y désignent une famille différente. Un code K0x cité sans son millésime ne veut donc rien dire. La table de correspondance entre les deux versions est en §3.11.1, et tous les codes de ce rapport sont ceux de 2025.

flowchart TB
    subgraph Q1["Question : est-ce bien configuré ?"]
        CIS["<b>CIS Benchmark</b><br/>contrôles numérotés<br/>kube-bench, prowler"]
        NSA["<b>NSA/CISA Hardening</b><br/>durcissement des charges<br/>kubescape"]
    end
    subgraph Q2["Question : par où peut-on m'attaquer ?"]
        MITRE["<b>MITRE ATT&CK</b><br/>tactiques x techniques<br/>kubescape + jeu manuel"]
    end
    subgraph Q3["Question : ai-je un angle mort ?"]
        OWASP["<b>OWASP K8s Top 10</b><br/>grille de relecture<br/>lecture manuelle"]
    end
    CIS --> CONSTAT["Constats<br/>(section 3)"]
    NSA --> CONSTAT
    MITRE --> PT["Scenarios purple team<br/>(section 4)"]
    MITRE --> CONSTAT
    PT --> CONSTAT
    CONSTAT --> OWASP
    OWASP --> FINAL["Rapport final"]

La logique : les référentiels de configuration disent ce qui devrait être vrai, MITRE dit ce qu'un attaquant essaierait, et le purple team mesure ce qui se passe vraiment.

1.5 Comment lire les chiffres de ce rapport

C'est la section la plus importante de ce chapitre. Les cinq points qui suivent sont les raisons pour lesquelles les chiffres de ce rapport ne sont pas ceux que les outils affichent.

1.5.1 Deux référentiels au même score n'évaluent pas la même chose

Sur le homelab, même scan, même outil, même jour :

Framework Score Contrôles du référentiel Évalués (2 sur 26 exigent l'opérateur en cluster) En échec, sur les évalués
MITRE ATT&CK 61,92 / 100 26 24 18
NSA/CISA 61,96 / 100 26 24 20

Les deux notes sont à 0,04 point l'une de l'autre. On pourrait en conclure que l'un des deux référentiels est redondant. La mesure dit le contraire :

comm -12 mitre.ids nsa.ids | wc -l   # 12 contrôles communs
comm -23 mitre.ids nsa.ids | wc -l   # 14 propres à MITRE
comm -13 mitre.ids nsa.ids | wc -l   # 14 propres à NSA

12 contrôles sur 26 seulement sont partagés. Plus de la moitié de chaque référentiel est propre. Les deux notes se ressemblent par coïncidence de moyenne, pas par identité de contenu.

📌 Deux référentiels qui donnent le même score ne se valident pas l'un l'autre. Il faut comparer les listes de contrôles, pas les notes.

Conséquence pour la section 5 : les scores kubescape bruts des deux clusters ne seront jamais comparés. Sur le homelab, les contrôles qui interrogent l'API AWS échouent faute de fournisseur cloud ; sur EKS ils s'évaluent. Le nombre de contrôles évalués diffère. La comparaison se fait contrôle par contrôle, sur l'intersection.

1.5.2 La sévérité d'un outil mesure l'étendue, pas la gravité

C-0066 (les secrets ne sont pas chiffrés dans etcd) et C-0067 (aucun journal d'audit) sont classés Medium par kubescape. Ce sont pourtant les deux constats les plus graves du homelab.

La raison est arithmétique : ils ne touchent qu'une seule ressource, le kube-apiserver. Le score pondère par le nombre de ressources concernées. Or le plan de contrôle est une ressource unique dont la défaillance est globale.

Les criticités de ce rapport sont donc réattribuées à la main, jamais recopiées de l'outil.

1.5.3 Compter des lignes n'est pas compter des causes

Le rapport MITRE de prowler sur le compte AWS produit 589 lignes, dont 157 en échec.

🔴 157 échecs ne sont pas 157 problèmes. Prowler écrit une ligne par couple (contrôle × technique MITRE). Un même contrôle en échec compte autant de fois qu'il est rattaché à des techniques.

Mesure Valeur
Lignes en échec 157
Contrôles distincts en échec 41
Techniques MITRE touchées 32

Et quatre contrôles à eux seuls colorent la moitié de la matrice :

Contrôle Techniques qu'il fait rougir
config_recorder_all_regions_enabled 17
guardduty_is_enabled 12
iam_aws_attached_policy_no_administrative_privileges 10
iam_policy_allows_privilege_escalation 10

Écrire « 32 techniques MITRE exposées » serait une inflation malhonnête. La phrase juste est : quatre causes racines expliquent l'essentiel de la surface, et trois d'entre elles sont l'absence d'un service de détection.

1.5.4 Un taux de conformité qui ignore des contrôles n'est pas un taux de conformité

Sur les 589 lignes du rapport MITRE, 42 sont marquées MANUAL. Ce sont des contrôles que l'outil ne peut pas trancher seul. Ni réussis, ni échoués, ils sortent silencieusement du calcul de conformité, soit 7 % des contrôles.

De même, le score NSA/CISA du homelab annoncé à 62/100 repose sur une couverture de 92 % : 24 contrôles évalués sur 26. Les deux manquants exigent l'opérateur Kubescape déployé dans le cluster, que la ligne de commande ne remplace pas.

⚠️ Un chiffre à ne pas confondre avec la couverture : parmi les 24 contrôles réellement évalués, 83 % sont en échec (20 sur 24). C'est ce second chiffre, le taux d'échec, qui explique le score bas — pas la couverture, qui est en réalité bonne. Deux mesures différentes, corrigées ici après recalcul direct sur le fichier source (24/26 = 92 %, pas 83 %, erreur trouvée à la relecture du 24/08).

Annoncer 62/100 sans le dire serait malhonnête. Chaque score de ce rapport porte donc sa couverture.

1.5.5 Un titre de contrôle n'est pas un constat

C'est la règle de lecture qui a évité le plus de constats faux. Elle se lit dans la colonne STATUS_EXTENDED de prowler, jamais dans le titre.

Deux cas, symétriques, et c'est ce qui rend la paire démonstrative :

17/08 18/08
Contrôle iam_root_hardware_mfa_enabled iam_user_hardware_mfa_enabled, sur youss_admin
Titre lu naïvement « le compte racine n'a pas d'authentification forte » « cet utilisateur n'a pas d'authentification forte matérielle »
Réalité le compte racine a une MFA virtuelle, le CIS exige du matériel. Écart mineur youss_admin n'a aucune MFA, d'aucune forme
Le titre était plus grave que la réalité plus doux que la réalité
Il aurait produit un constat faux à charge un constat faux à décharge

La règle n'est donc pas « les titres exagèrent ». C'est « les titres ne sont pas des constats ». Elle a été appliquée dans les deux sens, pas seulement quand elle arrangeait.

⚠️ À ne pas généraliser aux cinq comptes. Ce même contrôle échoue aussi sur iamadmin, mais pour un motif différent : STATUS_EXTENDED y dit « a une MFA virtuelle plutôt que matérielle », pas « aucune MFA ». C'est justement le compte que AW-9 détaille : une MFA existe sur la console, mais AWS ne l'exige jamais sur un appel d'API à la clé. Les deux constats se complètent, ils ne se contredisent pas.

1.5.6 Un avertissement d'outil qu'on lit quand même comme une réponse

Sur EKS, chaque identité IAM franchit un autorisateur webhook avant le RBAC natif : l'API Kubernetes délègue la question « qui es-tu ? » à IAM, puis rejoue le RBAC habituel sur l'identité qui en revient. C'est ce détour qui casse une commande utilisée sans incident tout au long de ce rapport.

Étalonnage, sur la même identité, en même temps :

Test Commande Attendu Obtenu
A — question fermée kubectl auth can-i <verbe> <ressource> --as=<ARN IAM> yes yes
B — question ouverte kubectl auth can-i --list --as=<ARN IAM> la même liste de droits rien, seulement les URL publiques et le self-review

A répond, B se tait sur la même identité. Et l'outil le dit lui-même, dans un avertissement qu'on lit d'habitude sans y prêter attention :

Warning: the list may be incomplete: webhook authorizer does not support user rule resolution

--list s'appuie sur un SelfSubjectRulesReview, une revue en interne du RBAC. Elle ne sait pas rejouer l'autorisateur webhook, donc elle ne voit aucun droit accordé par une Access Entry AWS. can-i <verbe> <ressource> interroge un SubjectAccessReview complet, qui rejoue tout le chemin et répond juste.

Conséquence pour la méthode de cet audit. Toute question sur une identité IAM d'EKS est posée en question fermée, un verbe et une ressource à la fois, jamais en --list. C'est pour cette raison que les tableaux d'accès de la section 3.3 (EK-10) sont construits ligne par ligne plutôt que par une seule commande d'inventaire : une commande unique aurait produit une liste vide, lue à tort comme « aucun droit ».

Un outil qui déclare son incomplétude, lu quand même comme une réponse complète : c'est la forme la plus discrète du motif de la section 1.5.5, un avertissement au lieu d'un titre.

1.6 Outillage et rejouabilité

Tout l'outillage tourne en conteneur. Rien n'est installé sur la machine.

Deux raisons. Une commande docker run à tag épinglé est rejouable par le lecteur du rapport (annexe 7.1), un apt install ne l'est pas. Et l'audit ne laisse aucun binaire derrière lui.

Outil Image épinglée Rôle
kube-bench docker.io/aquasec/kube-bench:v0.16.0 CIS Kubernetes Benchmark
kubescape quay.io/kubescape/kubescape-cli:v4.0.12 MITRE ATT&CK et NSA/CISA
prowler docker.io/toniblyx/prowler:5.39.0 CIS AWS et 52 autres référentiels
témoin docker.io/busybox:1.36 pod témoin des scénarios purple team

Digest de l'image prowler, pour la rejouabilité stricte : sha256:58c8a0eb0c947517bd89b6214cde0cc1d5f59df4eebbb99a87475ab741914959

Trois pièges de tag rencontrés, chacun aurait fait perdre du temps le jour de la mesure :

  1. quay.io/kubescape/kubescape:* est l'image de l'opérateur déployé dans le cluster, pas l'outil en ligne de commande. Lancée à la main, elle réclame /etc/config/clusterData puis meurt. Le CLI est kubescape-cli.
  2. kubescape-cli:latest rend la v3.0.46 alors que la dernière version est la v4.0.12. Le tag latest ne pointe pas la dernière version.
  3. toniblyx/prowler:5.40.0 n'existe pas. Le tag latest contient bien 5.40.0, mais le dernier tag versionné publié est 5.39.0.

Les outils déjà en place dans les chaînes d'intégration continue des deux projets ont été réutilisés tels quels : Trivy, tfsec, kube-linter, gitleaks, semgrep, Renovate.

1.7 Garanties de non-nuisance

Un audit peut casser ce qu'il mesure. Quatre garanties ont été posées, chacune vérifiée.

Garantie Comment elle est prouvée
L'outil d'audit du compte AWS ne pouvait pas modifier le compte rôle dédié prowler-audit, permissions SecurityAudit + ViewOnlyAccess. Incapacité d'écrire prouvée par test négatif : ec2:CreateSecurityGroup --dry-run rend UnauthorizedOperation pendant qu'une lecture passe
Aucun résultat n'a été envoyé vers un service tiers l'option --submit de kubescape est désactivée par défaut, vérifié dans l'aide de l'outil. Aucun résultat ne part vers le SaaS de l'éditeur
Aucune valeur de secret n'a été affichée seuls les noms de clés et les longueurs ont été relevés
Toute ressource témoin créée a été supprimée, et la suppression vérifiée contrôle explicite NotFound, pas « la commande de suppression a été lancée »

🔴 Le rôle prowler-audit doit être supprimé à la clôture de l'audit. Les commandes sont dans ~/.aws/prowler-role-setup.sh.

1.8 Index des identifiants

Chaque constat porte un identifiant. C'est lui qui permet de suivre ce qui reste ouvert, mois après mois, sans relire le rapport.

Préfixe Périmètre
C-* cluster homelab
EK-* cluster EKS
AW-* compte AWS
PT-* scénario purple team, voir la table dédiée en section 4.2
RE-* pas un constat : une étape numérotée du script RBAC de la journée live (« bloc R »), qui produit la preuve d'un ou plusieurs constats EK-*/C-*/AW-* cités à côté. Cité en section 3.3

Index complet, une ligne par constat. À garder sous la main pendant la lecture.

Cluster homelab

ID En une ligne Criticité Section
C-A le détecteur de dérive CIS ne tourne pas quand le cluster est éteint 🔵 Faible 3.1
C-B le verdict calculé de kube-bench discrimine vraiment 🟢 Point fort 3.1
C-D score NSA/CISA de 62/100, sur une couverture de 92 % (24/26), et 83 % d'échec parmi les évalués : deux chiffres à ne pas confondre — mesure 1.5.4
C-E la vérification des secrets porte sur les noms de clés, pas sur chaque valeur : une conclusion bornée, pas absolue — méthode 1.3.7
C-F deux pods fantômes après une extinction brutale 🔵 Faible, résolu le 18/08 — (clos avant le gel du périmètre, pas de section dédiée)
C-G ~/.aws sur un montage Windows qui ignore les permissions Unix 🟡 Moyenne, corrigé le 18/08 3.8
C-L le dépôt cockpit pouvait être poussé vers un dépôt public 🟡 Moyenne, corrigé le 17/08 3.7
C-M aucune sauvegarde d'etcd, et un seul plan de contrôle : le cluster n'est pas restaurable 🟠 Élevée 3.6
C-N le poste d'administration concentre les clés des deux infrastructures 🟠 Élevée 3.8
C-O aucun journal d'audit de l'API : aucune investigation rétroactive possible 🟠 Élevée 3.1
C-P les secrets sont stockés en clair dans etcd 🟠 Élevée 3.2
C-Q l'agent de collecte alloy peut lire tous les secrets du cluster, et tourne en root 🟠 Élevée 3.3
C-R 13 namespaces sur 14 sans aucune politique réseau 🟠 Élevée 3.4
C-S 73 conteneurs de plateforme sans limite mémoire, dont kube-apiserver 🟠 Élevée 3.6
C-T alloy-traces tourne en root sans en avoir besoin 🟡 Moyenne 3.5
C-U ArgoCD est cluster-admin : compromettre le dépôt synchronisé ferme la chaîne jusqu'à root sur les nœuds 🔴 Critique 3.3
C-V cert-manager peut créer et supprimer un pod, et supprimer un Service, dans les 14 namespaces 🟠 Élevée 3.3
C-W un droit de test accordé pendant l'audit a survécu à l'identité qui l'a reçu, corrigé sur-le-champ 🟡 Moyenne, corrigé en vol 3.3
C-X kube-system n'a aucun label d'admission, décision documentée mais jamais écrite comme telle 🟡 Moyenne 3.5
C-Y le trafic interne circule en clair, y compris ArgoCD en --insecure 🟡 Moyenne 3.4

Cluster EKS

ID En une ligne Criticité Section
EK-1 les journaux du plan de contrôle sont activés 🟢 Point fort 3.1
EK-2 le chiffrement KMS d'etcd est désactivé, et sa justification écrite est fausse 🟡 Moyenne 3.2
EK-3 les 8 namespaces de plateforme n'ont ni admission ni politique réseau 🟡 Moyenne 3.4
EK-4 cinq clés d'accès statiques, aucune rotation déclarée (révisé le 19/08, le dépôt seul en annonçait trois) 🟠 Élevée 3.3
EK-5 aucun flux VPC journalisé, confirmé une fois le réseau du cluster monté 🟡 Moyenne 3.1
EK-6 le groupe de sécurité de la base s'ouvre à tout le VPC 🟡 Moyenne 3.4
EK-7 la base n'a ni sauvegarde ni protection contre la suppression 🔵 Faible, arbitrage assumé 3.6
EK-8 les points forts du durcissement, vérifiables dans le dépôt 🟢 Point fort 3.5
EK-9 deux consoles d'administration exposées sur Internet, dont ArgoCD 🟠 Élevée 3.4
EK-10 la CI garde un accès au cluster qu'elle n'utilise plus 🟡 Moyenne 3.3
EK-11 un commentaire du dépôt affirme le contraire du dépôt 🔵 Faible 3.7
EK-12 créer un pod hors admission ouvre 16 chemins réels, pas 64 🟡 Moyenne 3.3
EK-13 une cinquième identité lit tous les secrets, sans équivalent au homelab 🟡 Moyenne 3.3
EK-14 neuf liaisons RBAC orphelines, livrées par la distribution elle-même 🔵 Faible 3.3
EK-15 le dépôt est public en lecture seule, mais un push direct sur la branche que suit ArgoCD n'exige ni revue ni pipeline vert 🔵 Faible 3.7
EK-16 les clés du compte AWS sont servies aux pipelines de toutes les branches, et le correctif évident casserait les MR 🟠 Élevée 3.7
EK-17 la version du plan de contrôle est épinglée depuis trois mois et demi, et le bot de mise à jour du projet ne la regarde pas 🟠 Élevée 3.7

Compte AWS

ID En une ligne Criticité Section
AW-1 cinq services de détection existent, aucun n'est activé 🟠 Élevée 3.1
AW-2 une clé de CI compromise donne le cluster, puis un chemin vers le compte 🔴 Critique 3.3
AW-3 cinq clés statiques actives, aucune rotation, aucune sous trente-neuf jours (révisé le 19/08, le dépôt seul en annonçait trois) 🟠 Élevée 3.3
AW-4 un compte machine sans authentification forte, tranché : le sujet est sa clé statique 🔵 Faible 3.3
AW-5 le bucket du state Terraform n'impose ni HTTPS ni MFA à la suppression 🟡 Moyenne 3.2
AW-6 le groupe de logs EKS survit à la destruction du cluster 🟢 Point fort 3.1
AW-7 aucune politique de mot de passe, et elle ne protégerait qu'un compte : l'administrateur 🟡 Moyenne 3.3
AW-8 le compte racine a une MFA virtuelle et non matérielle 🟢 Point fort nuancé 3.9
AW-9 la clé d'accès de l'administrateur donne le compte entier sans MFA, aucune politique ne l'impose 🔴 Critique 3.3
AW-10 sept secrets jamais tournés, et aucune politique de ressource pour arrêter un administrateur 🟠 Élevée 3.2
AW-11 le cloisonnement par environnement est contourné par le rôle du contrôleur ESO 🟠 Élevée 3.3

⚠️ Identifiants retirés, à ne plus citer

Deux dettes de numérotation, soldées à la rédaction. La table de correspondance complète est en section 3.10.

Retiré Remplacé par Pourquoi
C-H, C-I, C-J, C-K AW-1, AW-8, AW-3, AW-5 ils portaient sur le compte AWS, or le préfixe C-* désigne le homelab
C-C C-O (journal d'audit) et C-P (chiffrement etcd) il mélangeait deux constats distincts dans un seul identifiant, ce qui rendait son suivi impossible : on ne peut pas fermer la moitié d'un constat

2. Architecture auditée

2.1 Cluster homelab (kubeadm auto-hébergé)

2.1.1 Substrat

Hyperviseur Proxmox, 192.168.1.250
Machines 3 machines virtuelles
Plan de contrôle k8s-cp-1, 192.168.1.50, unique
Nœuds de travail k8s-worker-1 (.51), k8s-worker-2 (.52)
Version Kubernetes v1.36.2
CNI Cilium en eBPF, kube-proxy supprimé
Stockage local-path-storage, plus MinIO pour les sauvegardes

Le CNI (Container Network Interface) est le composant qui donne une adresse réseau aux pods et applique les politiques réseau.

🔴 Un seul plan de contrôle, donc un seul etcd. C'est le fait structurant de cette architecture, et il commande les constats C-M, C-P et C-S.

2.1.2 Namespaces et admission

Un namespace est un cloisonnement logique de Kubernetes. Le PSA (Pod Security Admission) est le mécanisme qui refuse à l'admission un pod trop privilégié. Il a trois niveaux : privileged (le plus permissif, il n'interdit rien), baseline, et restricted (le plus strict).

13 namespaces sur 14 sont étiquetés, versions épinglées v1.36, avec warn et audit un cran au-dessus d'enforce.

Niveau enforce Namespaces Compte
restricted argocd, cert-manager, default, envoy-gateway-system, kube-node-lease, kube-public, sealed-secrets, todos-demo 8
baseline minio 1
privileged kube-bench, local-path-storage, metallb-system, monitoring 4
aucun label kube-system 1

⚠️ privileged est un label, mais il n'impose rien. Le compter comme « couvert » serait faux. La couverture réelle de l'admission est de 9 namespaces sur 14, pas 13.

2.1.3 Exposition réseau

Une seule porte d'entrée : un unique service de type LoadBalancer, la Gateway Envoy, sur 192.168.1.201, ports 80 et 443.

🟢 Cette adresse est une adresse privée, et la non-exposition est prouvée, pas déduite. Toutes les consoles, Grafana, MinIO et ArgoCD, ne sont joignables que depuis le réseau local. Quatre ports testés depuis une connexion extérieure, quatre inaccessibles, témoin positif : section 5.4 bis.

C'est un point fort majeur, et c'est aussi la raison pour laquelle le quatrième axe du comparatif ne dit pas ce qu'on croyait.

Nuance à ne pas omettre. MetalLB, le composant qui attribue l'adresse du LoadBalancer, alloue aussi des NodePort (32475 et 31560). Le service est donc joignable par <ip-d-un-noeud>:32475 en plus de l'adresse annoncée. C'est une seconde voie d'accès, souvent oubliée parce qu'elle n'apparaît pas dans l'adresse publiée.

2.1.4 Charges applicatives

Une seule application métier : todos-demo, backend FastAPI et frontend React, déployée par ArgoCD depuis le dépôt. Elle sert d'application de démonstration à la chaîne d'observabilité.

2.1.5 Chaîne d'observabilité

Prometheus, Loki, Grafana, Tempo, et l'agent de collecte alloy déployé en DaemonSet, donc présent sur les trois nœuds. Cette chaîne existe, elle fonctionne, et elle survit à l'extinction du cluster : les verdicts de kube-bench écrits avant une extinction sont toujours interrogeables après. C'est mesuré, pas supposé.

2.1.6 Schéma

Aucun schéma d'architecture du homelab n'existait dans aucun dépôt. Celui-ci est produit pour ce rapport, à partir de l'inventaire inventaire-homelab-2026-08-19/, capturé cluster allumé le 19/08, et non depuis les manifestes. Trois vues de la même infrastructure : le substrat, le chemin d'entrée, et la chaîne d'observabilité.

Vue 1 — substrat et chemin d'entrée
flowchart TB
    NET["Freebox · 192.168.1.254<br/>passerelle et unique DHCP du LAN<br/>aucune redirection de port entrante"]

    subgraph PVE["Hyperviseur Proxmox VE · 192.168.1.250 · 128 Go RAM, 32 vCPU · datastore nvme-thin"]
        direction LR
        CP["k8s-cp-1 · 192.168.1.50<br/>plan de contrôle + etcd<br/>UNIQUE, sans sauvegarde"]
        W1["k8s-worker-1 · 192.168.1.51"]
        W2["k8s-worker-2 · 192.168.1.52"]
    end

    LB["Gateway API Envoy 'eg'<br/>seul Service de type LoadBalancer du cluster<br/>192.168.1.201 · 80 puis NodePort 32475 · 443 puis 31560<br/>adresse attribuée par MetalLB en mode L2"]

    R1["HTTPRoute argocd<br/>argocd.homelab.local"]
    R2["HTTPRoute grafana<br/>grafana.homelab.local"]
    R3["HTTPRoute todos<br/>todos.homelab.local"]

    A1["argocd-server<br/>ClusterIP"]
    A2["kube-prometheus-stack-grafana<br/>ClusterIP"]
    A3["todos-frontend puis todos-backend puis postgres<br/>ClusterIP"]

    NET --- PVE
    CP -.- LB
    W1 -.- LB
    W2 -.- LB
    LB --> R1 --> A1
    LB --> R2 --> A2
    LB --> R3 --> A3

Ce que la vue 1 établit.

Fait Conséquence d'audit
192.168.1.201 est une adresse privée, et la box ne redirige aucun port surface d'attaque externe nulle, vérifié par test négatif depuis une connexion mobile (section 5.4)
un seul Service LoadBalancer, tout le reste en ClusterIP une seule porte à surveiller, mais aussi un seul point de panne pour les trois applications
un seul plan de contrôle, non sauvegardé constat C-M : le cluster n'est pas restaurable. C'est le fait structurant du homelab
MetalLB annonce l'IP en mode L2 (le nœud élu répond aux requêtes ARP du réseau) pas de routage BGP, donc le trafic entrant traverse un seul nœud à la fois
Vue 2 — les 14 namespaces par niveau d'admission (PSA)

Le PSA (Pod Security Admission, le contrôle qui refuse à l'admission un pod trop permissif) range chaque namespace à l'un de trois niveaux : restricted (le plus strict), baseline (interdit l'essentiel), privileged (ne refuse rien).

flowchart LR
    subgraph R["enforce = restricted · 8 namespaces"]
        direction LR
        r1["argocd<br/>GitOps"]
        r2["todos-demo<br/>application de démonstration"]
        r3["envoy-gateway-system<br/>entrée du cluster"]
        r4["cert-manager"]
        r5["sealed-secrets"]
        r6["default · kube-public · kube-node-lease<br/>vides"]
    end
    subgraph B["enforce = baseline · 1 namespace"]
        b1["minio<br/>stockage objet S3"]
    end
    subgraph P["enforce = privileged · 4 namespaces"]
        p1["monitoring<br/>toute la chaîne LGTM"]
        p2["metallb-system"]
        p3["local-path-storage"]
        p4["kube-bench"]
    end
    subgraph N["AUCUN label PSA · 1 namespace"]
        n1["kube-system<br/>Cilium, CoreDNS, kube-proxy, metrics-server"]
    end

Ce que la vue 2 établit. Les namespaces les plus exposés sont les mieux contraints, et l'inverse est vrai aussi : monitoring, qui porte le plus de composants, ne refuse rien à l'admission, et kube-system n'a aucun label, donc aucun niveau appliqué. C'est la répartition qui compte ici, pas le décompte.

Vue 3 — chaîne d'observabilité
flowchart LR
    subgraph SRC["Sources, sur les 3 nœuds"]
        AL["alloy · DaemonSet<br/>collecte des journaux"]
        NE["node-exporter · DaemonSet<br/>métriques machine"]
        ALT["alloy-traces · Deployment<br/>réception des traces OTLP"]
    end
    subgraph STO["Stockage, namespace monitoring"]
        PR["prometheus<br/>StatefulSet · métriques"]
        LO["loki<br/>StatefulSet · journaux"]
        TE["tempo<br/>StatefulSet · traces"]
        AM["alertmanager<br/>StatefulSet · règles par défaut uniquement"]
    end
    MI["minio<br/>stockage objet S3, namespace minio"]
    GR["Grafana<br/>seule interface de lecture"]

    NE --> PR
    AL --> LO
    ALT --> TE
    LO --> MI
    TE --> MI
    PR --> AM
    PR --> GR
    LO --> GR
    TE --> GR

Ce que la vue 3 établit. La chaîne est complète, des trois signaux jusqu'au tableau de bord, et elle survit à l'extinction du cluster : les verdicts kube-bench écrits avant une extinction restent interrogeables après. En revanche alertmanager ne porte que ses règles par défaut, donc rien ne notifie personne : c'est la brique manquante, traitée en remédiation.

2.2 Cluster EKS (managé AWS)

Statut, mis à jour le 24/08. Cette section a été écrite avant la journée live (18-19/08), à partir du seul dépôt Terraform, sans appel à l'API AWS ni à l'API Kubernetes. La journée live du 23/08 a bien eu lieu depuis, mais cette section elle-même n'a pas été réécrite : les faits qu'elle porte ont été confrontés au cluster réel dans les constats qui les citent (section 3, RE-0 à RE-8), pas ici. Seul le tableau des identités (2.2.4) a été corrigé directement, après la révision du 19/08. Le reste reste déclaratif ; se fier à §3 pour la version mesurée.

2.2.1 Substrat

Service Amazon EKS, région eu-west-3
Cluster <cluster-eks>
Plan de contrôle managé par AWS, inaccessible
Groupes de nœuds deux : core, et observability porteur du taint workload=observability:NoSchedule
CNI Cilium en overlay, eBPF, kube-proxy supprimé au basculement
Mode d'authentification API_AND_CONFIG_MAP, donc Access Entries et aws-auth coexistent
Endpoint de l'API public, restreint par public_access_cidrs

Un taint est une marque posée sur un nœud qui empêche un pod de s'y planifier s'il ne porte pas la tolération correspondante.

2.2.2 Namespaces et admission

15 namespaces, créés par le playbook Ansible de bootstrap.

Groupe Namespaces PSA
Plateforme applicative fastapi restricted
Applicatif back fastapi-dev, fastapi-staging, fastapi-prod restricted
Applicatif front frontend-dev, frontend-staging, frontend-prod restricted
Plateforme monitoring, logging, argocd, cert-manager, external-secrets, external-dns, envoy-gateway-system, tracing aucun label
Hors périmètre projet kube-system

Le namespace fastapi ne porte aucun pod. C'est le foyer du Gateway partagé par les trois environnements, et du RBAC de l'utilisateur d'intégration continue.

La frontière est nette : ce qui est applicatif est couvert, ce qui est plateforme ne l'est pas. C'est exactement l'inverse de la répartition du homelab. Voir EK-3 et section 5.3.

2.2.3 Exposition publique

Point d'entrée unique : NLB → Envoy Gateway → pods. Un seul Gateway, dans le namespace fastapi, sert huit hosts en cross-namespace.

Host Cible Namespace
api-dev.devopsyouss.com back FastAPI fastapi-dev
api-staging.devopsyouss.com back FastAPI fastapi-staging
api.devopsyouss.com back FastAPI fastapi-prod
app-dev.devopsyouss.com front nginx frontend-dev
app-staging.devopsyouss.com front nginx frontend-staging
app.devopsyouss.com front nginx frontend-prod
🔴 grafana.devopsyouss.com console Grafana monitoring
🔴 argocd.devopsyouss.com console ArgoCD argocd

Redirection HTTP vers HTTPS en place, /metrics filtré au niveau de la route, certificat wildcard *.devopsyouss.com par validation DNS-01.

🔴 Six applications et deux consoles d'administration sur Internet. Voir EK-9.

2.2.4 Identités

Quatre utilisateurs IAM avec accès au cluster, les quatre porteurs d'une clé d'accès statique. Une clé statique est un identifiant de longue durée : elle ne tourne pas toute seule et n'expire pas. Un cinquième utilisateur (iamadmin) porte aussi une clé statique mais n'a aucun accès au cluster — voir EK-4/AW-3 en §3.3 pour les cinq clés du compte.

⚠️ Table corrigée le 24/08. La première version, tirée du seul dépôt Terraform, annonçait youss_admin sans clé — parce qu'aucune clé n'y est déclarée pour cet utilisateur. La clé existe bien sur le compte réel, créée hors Terraform : voir la révision datée du 19/08 en EK-4.

Nommage. Les noms IAM réels portent le préfixe <projet-eks>- (<projet-eks>-gitlab-ci, <projet-eks>-gitlab-ci-infra, <projet-eks>-cockpit-panel) : il n'apparaît que dans les tables qui citent une mesure exacte du compte (§3.3). Le reste du rapport utilise la forme courte (gitlab-ci, gitlab-ci-infra, cockpit-panel) — un seul utilisateur par nom, pas deux.

Utilisateur Clé statique Déclarée en Terraform ? Accès au cluster
gitlab-ci oui oui Access Entry Edit, sur le namespace fastapi uniquement
gitlab-ci-infra oui oui Access Entry ClusterAdmin, sur tout le cluster
cockpit-panel oui oui aucun
youss_admin oui non Access Entry ClusterAdmin, sur tout le cluster

RBAC écrit à la main : deux Roles seulement, tous les deux dans fastapi, portant sur les CRD d'External Secrets et sur les HTTPRoutes. Aucun autre Role, RoleBinding, ClusterRole ni ClusterRoleBinding dans le dépôt.

IRSA (IAM Roles for Service Accounts, le mécanisme qui donne une identité AWS à un pod sans lui confier de clé) : quatre rôles, et il faut distinguer deux chemins qui ne se ressemblent pas.

Chemin Rôle assumé Portée
Applicatif (fastapi-dev, -staging, -prod) <projet-eks>-app-<env>-irsa, un par environnement l'ARN exact du seul secret de l'environnement
Plateforme (fastapi, monitoring, argocd) <projet-eks>-eso-irsa, unique et partagé les 7 secrets, dev, staging et prod compris

La différence tient à une seule clé de manifeste. Les SecretStore applicatifs portent un bloc auth.jwt.serviceAccountRef, qui fait assumer à ESO le rôle du ServiceAccount de l'environnement. Les trois SecretStore de plateforme n'en portent pas : ESO lit alors avec son propre rôle de contrôleur. C'est ce que détaille le constat de la section 3.3.

2.2.5 Secrets

Étape Quoi
Source de vérité AWS Secrets Manager, 7 secrets : 4 de plateforme (app, grafana, alertmanager-slack, argocd) et 3 d'environnement (dev, staging, prod)
Transport External Secrets Operator. Authentifié par le ServiceAccount de l'environnement sur le chemin applicatif, par son propre rôle de contrôleur sur le chemin plateforme (§2.2.4)
Destination Secret Kubernetes fastapi-secrets, creationPolicy: Owner
Garde-fou la clé du manifeste de base est un placeholder inexistant : oublier de la patcher casse au démarrage au lieu de retomber silencieusement sur un secret partagé

2.2.6 Réseau

NetworkPolicy dans le dépôt 5, toutes dans les namespaces applicatifs
Namespaces plateforme 0 NetworkPolicy

Les cinq politiques sont un default-deny-all en ingress et egress dans chacun des deux namespaces applicatifs, plus allow-fastapi, allow-postgres et allow-frontend. Sur le cluster monté, ces 5 manifestes de base se répliquent aux 3 environnements (dev/staging/prod) via les overlays Kustomize, ce qui donne les 15 objets NetworkPolicy mesurés en §3.4 et §5.3 — pas un écart, la même définition multipliée.

⚠️ L'egress d'allow-fastapi ouvre les ports 5432 et 443 sans clause to, donc vers n'importe quelle destination.

2.2.7 Ressources AWS

Ressource Posture déclarée
RDS PostgreSQL privé, chiffré au repos, backup_retention_period = 0, mono-AZ, deletion_protection = false
Groupe de sécurité RDS ingress 5432 depuis tout le CIDR du VPC 10.0.0.0/16
ECR tags immuables, scan au push, chiffré AES256
Nœuds EC2 IMDSv2 obligatoire, hop_limit = 2
VPC 3 sous-réseaux publics, 3 privés, 1 NAT Gateway

2.2.8 Schéma

Le schéma du dépôt a été reconfronté au commit de gel le 2026-08-20. Il est périmé, et il ne peut pas entrer dans ce rapport.

Preuve, par les dates de dernière modification :

git log -1 --format="%h %ad" --date=short -- docs/architecture.drawio
# 79dfc67 2026-06-10   "update architecture diagrams to Sprint 3 state"
git log -1 --format="%h %ad" --date=short 5d4e85d
# 5d4e85d 2026-08-17   commit de gel

68 jours d'écart. Et l'écart n'est pas cosmétique : entre les deux, des composants entiers sont apparus dans l'infrastructure.

git diff --name-status 79dfc67 5d4e85d -- terraform/ k8s/ ansible/ | grep -E "^A"
Absent du schéma Ce que c'est
k8s/frontend/* le frontend n'existe pas sur le schéma, ni son Service, ni sa HTTPRoute, ni ses deux NetworkPolicy
k8s/overlays/{fastapi,frontend}/{dev,staging,prod}/ les trois environnements, avec leurs ResourceQuota
k8s/platform/argocd-apps/ (12 fichiers) toute la plateforme GitOps : root-app, les deux ApplicationSets, kube-prometheus-stack, loki, tempo, alloy, otel-collector
k8s/platform/argocd/{secretstore,externalsecret-admin}.yaml la chaîne External Secrets et l'exposition d'ArgoCD

Autrement dit, le schéma décrit une application unique dans un cluster sans GitOps ni observabilité. Ce n'est plus l'architecture auditée.

🔴 C'est une occurrence de plus de EK-11 (un écrit du dépôt qui affirme le contraire du dépôt), et la plus visible : un schéma est ce qu'un lecteur regarde en premier.

Décision. Ne pas reprendre le fichier .drawio pour ce rapport. Le schéma qui suit est produit depuis les sections 2.2.1 à 2.2.7, donc depuis le dépôt au commit de gel, comme celui du homelab. La remise à jour du .drawio du dépôt est une tâche du projet EKS, pas de l'audit, et elle est portée au plan de remédiation.

Vue 1 — chemin d'entrée public
flowchart TB
    NET["Internet"]
    DNS["Route 53 · devopsyouss.com<br/>certificat wildcard *.devopsyouss.com<br/>validation DNS-01"]
    NLB["NLB AWS<br/>redirection HTTP vers HTTPS"]
    GW["Envoy Gateway · namespace fastapi<br/>UN seul Gateway, 8 hosts en cross-namespace<br/>filtre /metrics au niveau de la route"]

    subgraph APP["Applicatif · PSA restricted · NetworkPolicy present"]
        direction LR
        AD["api-dev / app-dev<br/>fastapi-dev · frontend-dev"]
        AS["api-staging / app-staging<br/>fastapi-staging · frontend-staging"]
        AP["api / app<br/>fastapi-prod · frontend-prod"]
    end

    subgraph PLAT["Plateforme · AUCUN label PSA · 0 NetworkPolicy"]
        direction LR
        GRAF["grafana.devopsyouss.com<br/>console Grafana · monitoring"]
        ARGO["argocd.devopsyouss.com<br/>console ArgoCD · argocd"]
    end

    RDS["RDS PostgreSQL · prive<br/>backup_retention_period = 0<br/>deletion_protection = false<br/>SG ouvert a tout le VPC 10.0.0.0/16"]

    NET --> DNS --> NLB --> GW
    GW --> AD & AS & AP
    GW --> GRAF & ARGO
    AD & AS & AP --> RDS

Ce que la vue 1 établit. Six applications et deux consoles d'administration sortent par la même porte (EK-9). Les consoles sont dans les namespaces les moins protégés : ni admission, ni politique réseau (EK-3). La frontière de protection est exactement inversée par rapport au homelab, et c'est l'axe 3 du comparatif.

Vue 2 — identités et chemin des secrets
flowchart LR
    subgraph IAM["Utilisateurs IAM avec acces cluster · 4 cles statiques sur 4"]
        CI["gitlab-ci · cle statique<br/>Access Entry Edit · ns fastapi"]
        CII["gitlab-ci-infra · cle statique<br/>Access Entry ClusterAdmin"]
        CP["cockpit-panel · cle statique<br/>aucun acces cluster"]
        YA["youss_admin · cle statique<br/>non declaree en Terraform<br/>Access Entry ClusterAdmin"]
    end
    SMENV["AWS Secrets Manager<br/>3 secrets d environnement<br/>dev · staging · prod"]
    SMPLAT["AWS Secrets Manager<br/>4 secrets de plateforme<br/>app · grafana · argocd · slack"]
    IRSA["3 roles app-env-irsa<br/>chacun borne au seul<br/>secret de son environnement"]
    ESOR["1 role eso-irsa · partage<br/>lit les 7 secrets"]
    K8S["Secret fastapi-secrets<br/>ns fastapi-dev · -staging · -prod"]
    K8SP["Secrets de plateforme<br/>ns fastapi · monitoring · argocd"]

    CI --> KAPI["API Kubernetes"]
    CII --> KAPI
    YA --> KAPI

    SMENV --> IRSA
    IRSA -->|"ESO assume le role de l environnement<br/>SecretStore avec bloc auth.jwt"| K8S
    SMPLAT --> ESOR
    SMENV -.->|"chemin plateforme<br/>cloisonnement contourne · AW-11"| ESOR
    ESOR -->|"ESO lit avec son propre role<br/>SecretStore sans bloc auth"| K8SP

    style ESOR stroke:#c0392b,stroke-width:3px

Ce que la vue 2 établit. Le chemin des secrets est un point fort de cette architecture : aucune clé n'est confiée à un pod, IRSA porte l'identité, et sur le chemin applicatif chaque rôle est borné à son environnement. La flèche rouge est l'exception : le rôle du contrôleur ESO, emprunté par le chemin plateforme, lit les trois environnements. Constat en section 3.3.

Le point faible est au-dessus : gitlab-ci-infra est ClusterAdmin et porte une clé statique, c'est-à-dire un identifiant de longue durée qui ne tourne pas tout seul (EK-4, AW-2, AW-3).

2.3 Compte AWS

Compte <ACCOUNT_ID>
Région principale eu-west-3
Contrôles prowler exécutés, 17/08 230 (116 PASS, 109 FAIL, 5 MANUAL) — vérifié dans le CSV brut à la relecture du 24/08. Base du 225 PASS+FAIL cité en §4.6 (230 moins les 5 MANUAL, même scan)
Backend du state Terraform bucket S3 <bucket-tfstate>

🔴 Le scan du 17/08 a eu lieu infrastructure EKS détruite. Tout ce qui touche le cluster, ses groupes de sécurité, ses rôles de nœuds et son chiffrement n'a pas pu être évalué. Ce relevé porte sur le compte au repos : IAM, S3, CloudTrail, Secrets Manager.

Second scan joué le 2026-08-23, cluster monté. Son livrable est le diff CIS, pas le rapport MITRE, pour la raison expliquée en section 4.5 : le mapping MITRE de prowler décrit le compte, pas l'infrastructure, et rend le même verdict monté ou détruit. Diff détaillé en section 4.6.


3. Constats par domaine

Échelle de criticité de ce rapport. Elle est attribuée à la main, jamais reprise de l'outil, pour la raison donnée en section 1.5.2.

Niveau Sens
🔴 Critique chemin d'attaque complet, ou perte irréversible possible
🟠 Élevée absence d'un contrôle structurant, exploitable dans un scénario réaliste
🟡 Moyenne affaiblissement réel, exploitation conditionnée
🔵 Faible écart de bonne pratique, impact limité
🟢 Point fort contrôle présent et vérifié

Chaque constat porte : sa preuve, son impact, sa recommandation et son effort.


3.1 Journalisation et détection

C'est le domaine le plus dégradé, et il l'est sur les trois périmètres à la fois.

🟠 C-O — Le cluster homelab n'a aucun journal d'audit

Preuve, prise sur le cluster en marche :

kubectl -n kube-system get pod kube-apiserver-k8s-cp-1 \
  -o jsonpath='{.spec.containers[0].command[*]}' | tr ' ' '\n' | grep -E 'audit-'
# aucune sortie

Le kube-apiserver n'a ni --audit-policy-file, ni --audit-log-path. Le journal d'audit de Kubernetes, qui enregistre chaque appel à l'API, n'existe pas.

Confirmé par deux voies indépendantes :

Voie Résultat
CIS Kubernetes Benchmark, via kube-bench 4 contrôles en échec : 1.2.16, 1.2.17, 1.2.18, 1.2.19
NSA/CISA et MITRE, via kubescape contrôle C-0067 Audit logs enabled à 0 %

Impact. Aucune investigation rétroactive n'est possible sur ce cluster. Un incident constaté aujourd'hui ne peut pas être reconstitué.

Portée réelle, plus large que l'audit lui-même. Trois autres contrôles deviennent non-détectables pour la même cause : C-0031 (suppression d'events), C-0002 (exécution de commande dans un conteneur), C-0035 (rôles administratifs). Un attaquant qui supprime les events efface la seule trace restante, et cette suppression n'est elle-même pas tracée.

Ce constat est la cause racine de PT-7, qui avait mesuré à la main qu'un kubectl exec ne laisse aucune trace. On savait que, on sait maintenant pourquoi.

Recommandation. Politique d'audit sur le kube-apiserver, niveau Metadata au minimum, sortie vers un fichier collecté par alloy vers Loki. La chaîne de collecte existe déjà, seul le producteur manque.

Effort : moyen. Suivi dans l'issue #51 du projet homelab.

🟠 AW-1 — Cinq services de détection AWS, aucun n'est activé

Preuve :

Service État mesuré Sans lui, on ne peut pas…
CloudTrail aucun trail savoir quel appel d'API a été fait, par qui
GuardDuty not enabled être alerté d'un comportement anormal
Security Hub not enabled agréger les alertes en un point
AWS Config recorder disabled voir ce qui a changé, et quand
Inspector2 not enabled détecter une vulnérabilité sur les charges

Aucune alarme CloudWatch d'aucune sorte. Les quinze contrôles cloudwatch_log_metric_filter_* échouent tous avec le même motif : « No CloudWatch log groups found with metric filters or alarms associated ». Y compris celui qui surveille l'usage du compte racine.

Trois sources concordent : le scan prowler, le dépôt Terraform (EK-5, aucune ressource aws_cloudtrail, aws_flow_log, aws_guardduty_detector ni aws_config_*), et la matrice MITRE. Ce n'est pas un oubli de configuration, ce n'est écrit nulle part.

Impact. Le compte AWS ne journalise rien et ne détecte rien. Combiné à AW-2, cela signifie qu'un chemin d'escalade de privilèges peut être parcouru sans qu'aucun signal ne soit produit.

Recommandation. CloudTrail multi-région d'abord, c'est le socle : sans lui, aucun des autres ne sert. Puis GuardDuty. Les deux se déclarent en Terraform, ce qui les rend rejouables à chaque montée.

Effort : faible pour CloudTrail et GuardDuty. Attention au coût de GuardDuty sur une infrastructure éphémère montée et détruite quotidiennement.

🟢 EK-1 — Les journaux du plan de contrôle EKS sont activés

Preuve, déclarative : enabled_cluster_log_types = ["api", "audit", "authenticator"] dans le module Terraform du cluster. Les journaux partent vers CloudWatch.

C'est l'exact opposé de C-O. Sur EKS, un appel à l'API Kubernetes laisse une trace. Sur le homelab, non.

Confirmation partielle déjà acquise, et gratuitement : le scan prowler du 17/08 signale que le groupe de logs /aws/eks/<cluster-eks>/cluster n'est pas chiffré par une clé gérée par le client. Le défaut est mineur, mais l'information qui compte est ailleurs : le groupe de logs existe, et le scan a tourné cluster détruit depuis la veille. Le contenant survit à la destruction du cluster (AW-6).

✅ Confirmé en mesure le 2026-08-19, cluster détruit depuis trois jours

Ce constat n'est plus déclaratif. Interrogé sans cluster, donc sans coût :

Mesure Valeur
Groupe de logs /aws/eks/<cluster-eks>/cluster, créé le 2026-05-06
Flux présents dont kube-apiserver-audit-*
Dernier événement 2026-08-16T16:09:15Z, jour de la destruction du cluster
Volume accumulé 1,02 Gio
Rétention configurée 🟡 aucune, donc illimitée

Trois choses sont prouvées d'un coup, et la troisième était la question la plus chère du projet.

  1. Le groupe existe, ce que le dépôt seul ne prouvait pas.
  2. Il reçoit réellement, et notamment le journal d'audit de l'API : les flux kube-apiserver-audit sont là. enabled_cluster_log_types n'est pas resté une intention.
  3. 🔴 Le contenu survit à la destruction du cluster. Des événements du 16/08 sont lisibles le 19/08, alors que le cluster n'existe plus depuis trois jours. Ce n'est plus le contenant qui survit (AW-6), c'est le contenu.

Ce que ça vaut pour le projet. La question « la trace survit-elle au teardown ? » devait être tranchée le lendemain de la journée live, par une recherche à rejouer. Elle est tranchée maintenant, sans cluster, sans facture. Le jour J n'a plus qu'à confirmer sur un marqueur frais.

La nuance qui rend ce point fort inconfortable, et il faut l'écrire. Ce qui fait survivre la trace, c'est l'absence de politique de rétention. Personne ne l'a décidée. Une rétention normalement configurée à 7 ou 30 jours, sur une infrastructure détruite chaque soir, aurait effacé l'historique bien avant qu'un audit s'y intéresse.

C'est donc un contrôle qui fonctionne par défaut, pas par conception. Même motif que C-Q, où un droit excessif vient d'une valeur par défaut de chart : ici c'est une capacité favorable qui vient d'un réglage absent. Le motif est symétrique, et il dit la même chose : ce que personne n'a décidé gouverne le comportement réel.

Contrepartie mesurée : 1,02 Gio accumulés et facturés sans terme, pour un cluster qui n'existe que quelques heures par mois.

Recommandation. Poser une rétention explicitement, et la choisir assez longue pour couvrir un besoin d'investigation, par exemple 90 jours. Le geste ne renforce pas le contrôle, il le rend intentionnel et borne le coût. Effort : très faible.

🟡 EK-5 — Aucune journalisation ni détection AWS déclarée pour l'infrastructure réseau

Preuve, déclarative : recherche exhaustive dans le dépôt Terraform, aucune ressource aws_flow_log, aws_guardduty_detector ni aws_config_configuration_recorder. EK-1 couvre le journal d'audit du plan de contrôle EKS ; ce constat couvre tout ce qui l'entoure — le réseau, les volumes, le reste du compte vu depuis l'infrastructure elle-même plutôt que depuis IAM (AW-1).

Confirmé sur le compte le 2026-08-23, une fois le VPC du cluster monté (diff CIS, §4.6) : vpc_flow_logs_enabled est en échec. Aucun flux VPC n'est journalisé. Sans ce flux, une connexion réseau entre deux ressources du VPC ne laisse aucune trace, contrairement à un appel d'API Kubernetes (EK-1) ou à un appel d'API AWS avec CloudTrail activé (AW-1, toujours éteint).

Portée, à ne pas confondre avec AW-1. AW-1 couvre les services de détection du compte (CloudTrail, GuardDuty, Security Hub, Config, Inspector2), évaluables sans cluster monté. EK-5 couvre ce qui n'existe que si l'infrastructure réseau existe : impossible à mesurer avant le 23/08, ce qui explique pourquoi ce constat restait déclaratif jusqu'ici.

Impact. Combiné à PT-3 et PT-10 (§4.4) : le cloisonnement réseau bloque bien le trafic non autorisé, mais aucun flux, bloqué ou autorisé, ne laisse de trace. Un mouvement latéral réussi via PT-10 (sortie HTTPS non filtrée) ne serait visible dans aucun journal réseau.

Recommandation. Activer les VPC Flow Logs vers CloudWatch, au niveau du VPC entier. Une seule ressource Terraform (aws_flow_log), aucun redéploiement du cluster. Effort : très faible.

🔵 C-A — Le détecteur de dérive CIS ne tourne pas quand le cluster est éteint

Preuve :

CronJob Déclenchement prévu Exécution réelle
kube-bench-controlplane 2026-08-16 01:00 UTC 2026-08-17 16:37 UTC
kube-bench-nodes 2026-08-16 01:30 UTC 2026-08-17 16:37 UTC

Environ 39 h de retard (39 h 37 pour kube-bench-controlplane, 39 h 07 pour kube-bench-nodes). Les deux jobs se sont déclenchés seuls au rallumage, en rattrapage du créneau manqué, le cluster étant éteint depuis le 10/08.

Impact. Un contrôle hebdomadaire sur un cluster allumé par intermittence ne produit pas une mesure hebdomadaire. Il faut demander sa date à l'indicateur avant de lui faire confiance.

Point technique associé, à surveiller. startingDeadlineSeconds est absent des deux CronJobs. Sans conséquence sur un rythme hebdomadaire. Mais au-delà de 100 créneaux manqués, Kubernetes bloque le CronJob définitivement. Sur un rythme horaire, quatre jours d'extinction suffiraient.

Recommandation. Poser startingDeadlineSeconds, et lire le verdict avec sa date. Effort : très faible.

🟢 C-B — Le verdict calculé de kube-bench, lui, discrimine vraiment

Point fort à écrire tel quel, parce qu'il est rare.

Le script de scan compare les échecs observés à une baseline versionnée, par identifiant et non par nombre, et rend new=[] / resolved=[].

KUBE_BENCH_VERDICT profile=controlplane node=k8s-cp-1     status=ok new=[] resolved=[]
KUBE_BENCH_VERDICT profile=nodes        node=k8s-worker-1 status=ok new=[] resolved=[]
KUBE_BENCH_VERDICT profile=nodes        node=k8s-worker-2 status=ok new=[] resolved=[]

⚠️ status=ok ne veut pas dire « aucun échec CIS ». Il veut dire « aucune dérive par rapport à la baseline ». Les 5 échecs connus sont dans la baseline, donc attendus. Le verdict mesure le changement, pas la conformité. Écrit ainsi, c'est un bon indicateur. Lu comme un feu vert de conformité, c'est un indicateur qui ne discrimine pas.

Effet de bord mesuré, et favorable : la baseline des nœuds de travail est vide, et les deux rendent ok. Donc les corrections de durcissement appliquées hors dépôt ont survécu à l'extinction du 10/08. C'était supposé, c'est maintenant mesuré.


3.2 Secrets et chiffrement au repos

🟠 C-P — Les secrets du homelab ne sont pas chiffrés dans etcd

Preuve directe, la plus démonstrative de la campagne. Un secret témoin est créé, puis lu directement dans la base etcd sans passer par l'API Kubernetes :

preuve-SI-TU-LIS-CECI-EN-CLAIR-ETCD-NEST-PAS-CHIFFRE␦Opaque␦

Aucun marqueur k8s:enc. Secret témoin supprimé après le test, suppression vérifiée.

Confirmé par kubescape, contrôle C-0066 Secret/etcd encryption enabled à 0 %, et par le chemin exact du réglage manquant : --encryption-provider-config, absent de la ligne de commande du kube-apiserver.

Le malentendu à lever, et il est important. SealedSecrets chiffre les secrets dans le dépôt Git. Il ne protège rien dans le cluster : le contrôleur les déchiffre pour créer des objets Secret Kubernetes, qui atterrissent en clair dans etcd.

Le vrai vecteur n'est pas la lecture d'etcd. Elle suppose d'être déjà administrateur du plan de contrôle. Ce sont les copies d'etcd qui sortent du périmètre de protection : sauvegarde, snapshot de machine virtuelle, disque de la VM. Un seul de ces fichiers donne tous les secrets sans aucun droit Kubernetes, donc contourne entièrement le RBAC vérifié par PT-1.

Pourquoi c'est aggravé ici. Trois constats se composent :

  • C-P : les secrets sont en clair dans etcd.
  • C-M : il n'existe aucune sauvegarde d'etcd, et un seul nœud de plan de contrôle.
  • C-N : la clé privée SealedSecrets vit sur le poste d'administration.

Quiconque lit le disque de k8s-cp-1 lit tous les secrets du cluster.

Constat annexe favorable : l'image etcd est distroless, elle n'embarque aucun shell. Un attaquant qui y parviendrait n'y trouverait aucun outil.

Recommandation. Activer le chiffrement au repos par EncryptionConfiguration. Le geste est documenté et suivi côté projet homelab. Effort : moyen, il touche le plan de contrôle.

🟡 EK-2 — Le chiffrement KMS d'etcd est désactivé côté EKS, et sa justification écrite est fausse

Preuve : une directive #tfsec:ignore:aws-eks-encrypt-secrets dans le module EKS, justifiée en commentaire par : « les secrets applicatifs passent par ESO + AWS Secrets Manager, pas par etcd ».

C'est faux. Le travail d'External Secrets Operator est précisément de matérialiser un objet Secret Kubernetes (creationPolicy: Owner), consommé par le Deployment en envFrom.secretRef. Ce Secret vit dans etcd comme tous les autres. Secrets Manager est la source, pas la destination.

La nuance qui distingue les deux clusters, et qui fait de ce constat un paragraphe de la section 5 plutôt qu'un doublon :

Homelab EKS
Secret lisible en base64 avec le RBAC suffisant oui oui
Chiffrement enveloppe par secret (clé KMS gérée par le client) non non
Chiffrement du stockage sous-jacent non oui, par AWS au niveau du volume
Copies d'etcd hors du cluster oui, c'est le vrai vecteur non, le plan de contrôle est managé et inaccessible

Le même contrôle manque des deux côtés, l'exploitabilité n'est pas la même.

⚠️ Un second gisement de chiffrement, à ne pas confondre avec celui-ci. Le diff CIS du 23/08 (§4.6) montre ec2_ebs_volume_encryption en échec : le chiffrement par défaut des volumes EBS n'est pas activé au niveau du compte. Ce n'est pas la même ressource que la ligne « chiffrement du stockage sous-jacent » ci-dessus, qui porte sur le stockage du plan de contrôle managé (chiffré par AWS, hors du périmètre client). Ici, il s'agit des volumes des nœuds de travail eux-mêmes — système d'exploitation et stockage éphémère des pods. Sans réglage explicite, un nouveau volume EBS créé sur ce compte reste non chiffré par défaut. Recommandation : activer ebs_encryption_by_default au niveau de la région, un réglage de compte, aucun redéploiement. Effort : très faible.

Ce constat est d'abord un constat de méthode. Le commentaire décrivait une intention qui a cessé d'être vraie le jour où ESO est devenu le transport. Personne ne l'a relu depuis.

Recommandation. Deux gestes distincts : corriger le commentaire, qui égare le prochain lecteur ; et arbitrer explicitement le chiffrement KMS, dont le coût n'est pas nul sur une infrastructure montée et détruite chaque jour. Effort : très faible pour le commentaire.

🟡 AW-5 — Le bucket du state Terraform n'impose ni HTTPS, ni MFA à la suppression

Preuve, trois contrôles en échec sur le même bucket <bucket-tfstate> :

Contrôle Constat
s3_bucket_secure_transport_policy aucune politique de bucket, donc les requêtes HTTP en clair sont acceptées
s3_bucket_no_mfa_delete pas de MFA exigée pour supprimer une version
s3_bucket_object_lock pas de verrouillage d'objet

Impact. C'est le bucket qui porte l'état de l'infrastructure. C'est donc le fichier dont la suppression ou la corruption est la plus coûteuse de tout le compte.

Rapprochement direct avec le homelab, matière de la section 5 : le state du homelab vit sur MinIO au NAS, sauvegardé et chiffré. Deux projets, deux backends distants, deux postures de protection différentes pour le même type de fichier critique.

Recommandation. Une politique de bucket refusant aws:SecureTransport = false. C'est cinq lignes de Terraform. Effort : très faible.

🟠 AW-10 — Sept secrets de production, jamais tournés, et aucune seconde barrière

Constat. Les sept secrets <projet-eks>/* d'AWS Secrets Manager cumulent deux défauts, appliqués uniformément : aucune rotation (ni automatique, ni manuelle, aucun n'a jamais été tourné), et aucune politique de ressource. Les valeurs sont générées une fois par Terraform et vivent indéfiniment.

Preuve. Scan prowler du 2026-08-23, service secretsmanager : 21 échecs sur 28 contrôles.

Contrôle Sévérité Échecs
secretsmanager_automatic_rotation_enabled High 7
secretsmanager_has_restrictive_resource_policy High 7
secretsmanager_secret_rotated_periodically Medium 7
secretsmanager_secret_unused Medium 0 échec, 7 réussites

⚠️ Ce ne sont pas 21 problèmes, et pas non plus « 14 échecs High ». Ce sont 3 contrôles joués sur 7 secrets, et deux de ces contrôles mesurent le même fait : « rotation désactivée » et « jamais tournée » sont la même absence, vue deux fois. Le décompte honnête est 2 défauts distincts, donc 2 correctifs. Voir la section 1.5 sur la lecture des chiffres.

Une ambiguïté qu'il a fallu lever. Le message de prowler sur la politique de ressource est, mot pour mot : « does not have a resource-based policy or access to the policy is denied for the role prowler-audit ». Il ne discrimine pas entre « la politique n'existe pas » et « l'auditeur n'a pas eu le droit de la lire ». Tranché sur le code au commit de gel 5d4e85d : aucune ressource aws_secretsmanager_secret_policy, aucun put-resource-policy, et l'infrastructure est intégralement décrite en Terraform. Le constat est établi, ce n'est pas un artefact de droits.

Impact. Un secret AWS peut être protégé à deux endroits : la politique d'identité, portée par le rôle demandeur, et la politique de ressource, portée par le secret lui-même. Seule la première existe ici. Or un Deny posé sur la ressource ne se contourne pas par un Allow posé sur l'identité, AdministratorAccess compris.

Conséquence directe : la chaîne AW-2 (clé de CI compromise → escalade IAM) et le constat AW-9 (iamadmin porte AdministratorAccess sans MFA) débouchent aujourd'hui sur la lecture des sept secrets, dont DB_PASSWORD de production, la clé de signature des jetons JWT, l'administrateur Grafana et l'administrateur ArgoCD. Une politique de ressource refuserait ce chemin même à un administrateur du compte.

Portée à ne pas surestimer. Cette politique de ressource ne couperait pas la chaîne AW-11, où l'attaquant emprunte l'identité légitime du contrôleur ESO : une politique qui autorise ce rôle autorise aussi qui parvient à l'emprunter. La politique de ressource borne quelles identités AWS peuvent lire ; le contrôle d'admission et le RBAC bornent qui peut devenir ces identités. Deux étages distincts, deux corrections distinctes.

Aggravant, non détecté par prowler et trouvé dans le code. Les sept secrets portent recovery_window_in_days = 0 : suppression immédiate et irréversible, là où le défaut AWS laisse trente jours de récupération. Le choix est assumé et commenté dans le dépôt (permettre une recréation propre du stack éphémère), c'est un arbitrage documenté et non un oubli. Mais combiné à AW-2, rien ne s'oppose à la destruction et rien ne la rattrape.

Second point trouvé dans le code : aucun kms_key_id sur les sept secrets, donc chiffrement par la clé AWS par défaut aws/secretsmanager. Le contrôle d'accès à la clé ne peut pas servir de seconde barrière. Même motif qu'EK-2 (chiffrement d'enveloppe absent sur etcd) : le chiffrement existe, la maîtrise de la clé n'existe pas.

🟢 Point favorable. Les sept secrets sont consommés : zéro échec sur secretsmanager_secret_unused. Aucun secret abandonné qui traînerait avec une valeur encore valide, contrairement au motif des clés statiques d'EK-4. L'inventaire est maîtrisé, le cycle de vie ne l'est pas.

À rapprocher de AW-3 : la rotation est un manque systématique sur ce compte, pas un oubli isolé.

Recommandation. Deux correctifs, pas quatorze. 1. Une politique de ressource deny by default sur les sept secrets, refusant tout principal hors la liste explicite des rôles IRSA attendus. Effort : faible. 2. Une rotation, à commencer par le DB_PASSWORD de production, le seul dont la rotation automatique soit native côté RDS. Effort : moyen.


3.3 Contrôle d'accès et identités

🔴 AW-2 — Une clé de CI compromise donne le cluster, puis le compte

Le constat le plus fort de ce rapport. Il chaîne trois éléments indépendants, et chacun est prouvé par une source différente.

Preuve, maillon par maillon :

Maillon Fait Source
1 l'utilisateur gitlab-ci-infra porte une clé d'accès statique, servie aux pipelines de toutes les branches du dépôt dépôt Terraform, modules/iam/main.tf:251 ; exposition mesurée le 26/08, EK-16 (§3.7)
2 il porte une Access Entry ClusterAdmin sur tout le cluster dépôt Terraform, terraform/ephemeral/main.tf
3 la politique IAM qui lui est attachée permet une escalade de privilèges scan prowler, contrôle iam_policy_allows_privilege_escalation
4 rien ne le détecterait AW-1, GuardDuty et Config éteints
clé d'accès statique  →  utilisateur gitlab-ci-infra  →  ClusterAdmin sur EKS
                                                      →  escalade IAM sur le compte

Le libellé exact du troisième maillon :

Custom Policy arn:aws:iam::<ACCOUNT_ID>:policy/<projet-eks>-terraform-ci-policy
allows privilege escalation using the following actions: 'ec2:CreateLaunchTemplate' ...

Impact. Une clé de CI compromise ne donne pas seulement le cluster. Elle donne un chemin vers l'administration du compte AWS. Et le parcours de ce chemin ne produirait aucun signal.

Recommandation, dans cet ordre :

  1. Activer CloudTrail, pour que le chemin devienne au moins visible. C'est le geste le moins coûteux et le plus rentable.
  2. Restreindre la politique <projet-eks>-terraform-ci-policy aux actions réellement utilisées par les jobs Terraform.
  3. Remplacer la clé statique par une identité fédérée OIDC entre GitLab et AWS, qui supprime la clé plutôt que de la faire tourner.

Effort : faible (1), moyen (2), élevé (3).

🟠 AW-11 — Le cloisonnement par environnement est contourné par le rôle de son propre outil de distribution

Constat. Le dépôt sépare soigneusement dev, staging et prod : trois namespaces, trois secrets AWS distincts, et trois rôles IRSA, chacun borné à l'ARN exact du seul secret de son environnement. Ce cloisonnement fonctionne, et il fonctionne jusqu'au niveau IAM.

Il est contourné par un quatrième rôle : celui du contrôleur External Secrets Operator, <projet-eks>-eso-irsa, qui lit les sept secrets, dev, staging et prod compris.

Preuve, mesurée sur le cluster en marche le 2026-08-23.

Les quatre rôles existaient et tournaient. Ils sont visibles dans le scan prowler du même jour :

<projet-eks>-app-dev-irsa      + politique inline <projet-eks>-app-dev-secrets-read
<projet-eks>-app-staging-irsa  + politique inline <projet-eks>-app-staging-secrets-read
<projet-eks>-app-prod-irsa     + politique inline <projet-eks>-app-prod-secrets-read
<projet-eks>-eso-irsa          + politique inline <projet-eks>-eso-secrets-read

Politique de permissions du rôle de contrôleur, capturée sur le cluster : sept ARN de secrets. Politique de confiance du même rôle : un seul ServiceAccount autorisé, external-secrets:external-secrets, avec la bonne audience. Elle est bien écrite.

La différence tient à une clé de manifeste. Les SecretStore des trois namespaces applicatifs portent un bloc auth.jwt.serviceAccountRef : ESO demande un jeton pour le ServiceAccount de l'environnement et assume son rôle. Les trois SecretStore de plateforme (fastapi, monitoring, argocd) n'ont aucun bloc auth : ESO lit alors avec son propre rôle de contrôleur. Le Job de bootstrap de la base, dans le namespace fastapi, lit ainsi les mots de passe des trois environnements en une seule requête.

Le chemin exploitable, maillon par maillon :

# Maillon Source
1 huit identités peuvent créer un pod dans external-secrets RBAC interrogé sur le cluster
2 ce namespace n'applique aucun pod-security en mode enforce scénario purple team, section 4
3 un pod qui y démarre avec le ServiceAccount d'ESO obtient des identifiants AWS réels prouvé, section 4
4 ce rôle lit les secrets des trois environnements, production comprise politique capturée ci-dessus

Ce que le constat n'est pas. Ce n'est pas « le rôle a trop de droits » : il a exactement ce dont ESO a besoin pour faire son travail. Ce n'est pas non plus « il manque des rôles par environnement » : ils existent. Le constat est que le cloisonnement construit côté applicatif est annulé par le chemin de plateforme, et qu'aucun document du dépôt ne signale cette asymétrie.

C'est le prix d'un contrôleur centralisé : un composant qui distribue les secrets de tout le monde doit pouvoir les lire tous. La séparation soignée d'un étage est défaite par l'outil qui la sert.

Impact. Le cloisonnement dev / staging / prod est réel côté applicatif et nul par le chemin de plateforme. Un attaquant capable de créer un pod dans un namespace de plateforme n'a pas besoin de franchir la séparation : il passe en dessous.

La phrase à garder pour le comparatif (section 5) : au homelab, create pods sans admission donne root sur un nœud (C-U, C-V). Sur EKS, il donne en plus une identité dans le compte AWS. Le managé ne supprime pas ce risque, il lui ajoute une dimension.

Recommandation. Trois gestes, dans cet ordre de coût croissant.

  1. Poser pod-security.kubernetes.io/enforce sur external-secrets. C'est le maillon 2, c'est un label, et c'est de très loin le geste le moins cher. Effort : très faible.
  2. Retirer dev, staging et prod de la politique du rôle de contrôleur. Il n'a besoin que des quatre secrets de plateforme. Effort : faible.
  3. Donner au Job de bootstrap de la base son propre ServiceAccount et son propre rôle IRSA, ou sortir ce bootstrap du cluster pour en faire un geste d'exploitation. Effort : moyen.

⚠️ Note de méthode, à lire avec la section 1.5. La première version de ce constat, écrite le jour de la mesure, affirmait qu'« un seul rôle IRSA lit les trois environnements » et recommandait d'en créer un par environnement. Elle était fausse : la capture n'avait interrogé que le rôle du contrôleur, et les trois rôles par environnement existaient déjà. La mesure était juste, sa portée ne couvrait pas la question posée. Corrigé au dépouillement à froid, contre le code du dépôt au commit de gel. Le constat exact est plus intéressant que le constat approximatif, et il ne s'expose pas au démenti par une lecture du dépôt.

🟡 EK-12 — Créer un pod hors admission ouvre 16 chemins réels, pas 64

Constat, section RBAC de la journée live (RE-2). Combien d'identités peuvent créer un pod dans un namespace de plateforme, là où EK-3 montre qu'aucune admission ne les arrêterait ?

Le script rend un total brut de 64 : 8 ServiceAccounts capables de créer des pods, multipliés par les 8 namespaces de plateforme sans label PSA (monitoring, logging, argocd, cert-manager, external-secrets, external-dns, envoy-gateway-system, tracing).

⚠️ Écrire 64 aurait exposé le rapport à une réfutation en une commande. Même motif que les « 54 secrets lisibles » qui valaient 41 (C-Q). Un décompte qui ne trie pas ses lignes n'est pas un chiffre, c'est un artefact du script qui l'a produit.

Six des huit ServiceAccounts sont des contrôleurs natifs de Kubernetes : daemon-set-controller, job-controller, replicaset-controller, replication-controller, statefulset-controller, persistent-volume-binder. Créer des pods est littéralement leur fonction — un DaemonSet ou un Job ne peut pas exister sans qu'un contrôleur crée son pod. Ce n'est pas une configuration à corriger, c'est le fonctionnement normal du cluster.

Lecture Chemins
Total brut, script 64
Contrôleurs natifs de Kubernetes 48
Identités réellement en cause 16

Restent argocd-application-controller et cert-manager, chacun capable de créer un pod dans les 8 namespaces de plateforme : 2 identités × 8 namespaces = 16 chemins. Ce sont les mêmes deux identités que C-U et C-V au homelab (voir plus bas, confirmées sur EKS avec les mêmes charts).

Impact. C'est ce chiffre, 16, qui borne le maillon 1 de la chaîne AW-11 : ce n'est pas « huit identités arbitraires », c'est deux contrôleurs de plateforme dont la fonction exige déjà de créer des pods, sur un périmètre que rien à l'admission ne restreint (EK-3).

Recommandation. Voir les recommandations de C-U, C-V et AW-11, qui portent toutes sur ces deux mêmes identités. Effort : voir ces constats.

🟠 EK-4 et AW-3 — Cinq clés statiques actives, aucune rotation, aucune sous trente-neuf jours

⚠️ Constat révisé le 2026-08-19. La version tirée du dépôt annonçait trois clés. Le compte en porte cinq. Deux d'entre elles ne sont déclarées nulle part dans le code d'infrastructure. Motif constat-perime-ecrit-non-reverifie : le dépôt et le compte n'avaient jamais été confrontés.

Preuve, mesurée sur le compte le 2026-08-19 :

Utilisateur Clé Créée le Âge Déclarée en Terraform ? Console
iamadmin Active 2026-05-04 107 j non 🔴 oui
youss_admin Active 2026-05-19 92 j non non
<projet-eks>-gitlab-ci Active 2026-05-12 99 j ✅ oui non
<projet-eks>-gitlab-ci-infra Active 2026-05-17 94 j ✅ oui non
<projet-eks>-cockpit-panel Active 2026-07-11 39 j ✅ oui non

Aucune ressource de rotation nulle part, ni dans le dépôt, ni sur le compte.

Le titre de ce constat est plus doux que la mesure. Ce n'est pas « la clé de la CI » : le contrôle CIS iam_rotate_access_key_90_days échoue sur quatre comptes au 2026-08-23, et le plus ancien n'est pas une identité machine, c'est iamadmin à 110 jours.

La démonstration la plus nette du rapport : une posture qui se dégrade sans que personne n'agisse

Ce constat a été mesuré deux fois, à six jours d'écart, sur un compte où aucune action humaine n'est intervenue entre les deux scans.

Compte 2026-08-17 2026-08-23
iamadmin 104 j — échec 110 j — échec inchangé
<projet-eks>-gitlab-ci 97 j — échec 102 j — échec inchangé
<projet-eks>-gitlab-ci-infra 92 j — échec 97 j — échec inchangé
youss_admin 90 j — réussite 96 j — échec 🔴 bascule
<projet-eks>-cockpit-panel réussite réussite inchangé
compte racine réussite (aucune clé) réussite inchangé

Trois échecs deviennent quatre. Aucune configuration n'a changé. Le seul facteur est l'écoulement du temps : youss_admin a franchi le seuil des 90 jours pendant l'audit.

Cette bascule a été prédite par écrit le 2026-08-22, avant la mesure du 23. La note de préparation annonçait le franchissement du seuil et le passage de trois à quatre échecs. C'est ce qui rend la démonstration probante : la prédiction est datée d'avant, pas reconstruite après.

Ce que ça établit, et qui dépasse ce constat. Un taux de conformité n'a de sens qu'accompagné de sa date et de son dénominateur. La posture de ce compte s'est dégradée toute seule entre deux scans, et un scan ponctuel présenté sans date aurait donné deux verdicts contradictoires sur la même infrastructure inchangée. C'est la justification directe du périmètre gelé à une date écrite (section 1.2) et de la mise en garde de la section 1.5.

Ce que la révision ajoute, et c'est le plus utile. Trois clés sont gouvernées par le code : on sait qui les crée, on peut les faire tourner en changeant une ligne. Deux existent hors du code, créées à la main, dont celle du compte administrateur. Elles ne sont ni inventoriées, ni reproductibles, ni révocables par un terraform apply.

Aggravant : gitlab-ci-infra porte ClusterAdmin (AW-2), et iamadmin porte AdministratorAccess sans exigence de MFA (AW-9).

Recommandation. Fédération OIDC pour les quatre identités machine, ce qui supprime la clé au lieu de la faire tourner. Pour iamadmin, voir AW-9. Effort : élevé.

🟠 C-Q — L'agent de collecte de logs peut lire tous les secrets du cluster homelab

Le plus exploitable des constats du homelab.

Preuve, RBAC interrogé sur le cluster en marche :

kubectl auth can-i list secrets --as=system:serviceaccount:monitoring:alloy -A   # yes
kubectl auth can-i get  secrets --as=system:serviceaccount:monitoring:alloy -A   # yes

Source du droit :

ClusterRoleBinding/alloy -> ClusterRole/alloy
  rules: resources=["configmaps","secrets"] verbs=["get","list","watch"]

Ce qui rend ce constat sérieux, c'est le cumul de cinq faits :

  1. alloy est un DaemonSet, donc présent sur les trois nœuds.
  2. Il monte /var/log en hostPath, donc il lit le disque de l'hôte.
  3. Il tourne dans monitoring, le seul namespace non-système en PSA privileged.
  4. Il peut lire tous les secrets de tous les namespaces.
  5. 🔴 Il tourne en root, explicitement. Mesuré le 2026-08-19 : runAsUser: 0 est écrit dans le manifeste du DaemonSet. Ce n'est pas un défaut d'image ni une valeur par défaut, c'est une déclaration.

Le cinquième point ferme la chaîne. Un processus root, sur les trois nœuds, dans le seul namespace qui n'impose rien à l'admission, qui lit le disque de l'hôte et qui peut lire tous les secrets du cluster. Chacun de ces cinq faits est acceptable seul. Ensemble, ils décrivent le composant le plus puissant du cluster, et personne ne l'a voulu ainsi.

Compromettre ce pod, ou son image, donne la totalité des secrets du cluster. Or sa fonction réelle est de lire des fichiers dans /var/log. Il n'a aucun besoin métier de lire les secrets.

Ce qui rend ce constat représentatif, et transposable au lecteur : le droit vient du chart Helm par défaut. Il n'a pas été accordé volontairement. Personne n'a décidé ça. Toute installation du même chart porte le même droit.

Addendum du 2026-08-20 : prouvé par l'effet, et chiffré. Le constat ne reposait que sur auth can-i, donc sur la réponse de l'autorisateur. Un pod temporaire portant l'identité alloy a appelé l'API avec le jeton réellement monté dans le pod :

--- secrets de todos-demo, vus par le jeton du DaemonSet alloy :
        "name": "todos-secrets"
--- total, tous namespaces confondus : 54

HTTP 200, et le secret applicatif todos-secrets est nommé dans la réponse. Ce n'est plus un droit théorique, c'est une lecture effectuée. Seuls les noms ont été collectés, jamais les valeurs : un rapport d'audit qui cite un secret est lui-même une fuite.

🔴 Le décompte de 54 était faux, la contre-épreuve l'a montré. Le cluster contient 41 secrets :

kubectl get secrets -A --no-headers | wc -l   # 41

Pourquoi le premier chiffre était trop haut : il comptait les occurrences de "uid" dans la réponse JSON, or un secret peut en porter plusieurs, son propre metadata.uid et celui de chaque ownerReference. Le motif ne comptait donc pas des secrets, il comptait des identifiants.

Le chiffre publié est 41, obtenu par un décompte qui porte sur des objets et non sur du texte. Publier 54 aurait exposé le rapport à une réfutation en une commande. Le constat, lui, ne bouge pas : ces 41 secrets sont tous lisibles avec le jeton du DaemonSet.

Et alloy n'est pas seul. La même campagne montre que sealed-secrets/sealed-secrets-controller détient secrets [get list create update delete watch]. Son besoin est réel, il écrit les secrets qu'il déchiffre. Mais cela fait deux identités de plateforme capables de lire tous les secrets du cluster, dont une qui n'en a aucun besoin.

Recommandation. Retirer secrets du ClusterRole/alloy, ou le restreindre par resourceNames aux secrets réellement nécessaires. Contre-épreuve obligatoire : le même auth can-i doit répondre no. Effort : faible.

Confirmé sur EKS le 2026-08-23 (bloc R, RE-6). Réponse yes des deux côtés, même chart (alloy 1.10.0). Le constat cesse d'être un défaut du homelab, c'est bien un défaut de la valeur par défaut du chart.

--- code HTTP sur les secrets de fastapi-prod : 200
--- noms lus dans fastapi-prod : "fastapi-secrets"
--- total de secrets lisibles, tous namespaces confondus : 55 (voir réserve ci-dessous)

Le jeton réellement monté dans le pod alloy lit le secret applicatif de production, fastapi-secrets dans fastapi-prod, exactement le motif prouvé au homelab. Ce point-là ne souffre d'aucune réserve : le nom est lu explicitement, en clair, avec un code HTTP 200.

⚠️ Réserve sur le chiffre 55, trouvée à la relecture du 24/08. La commande qui le produit compte les occurrences de "uid": dans la réponse JSON (bloc-r-eks.sh, étape 3) — exactement la même méthode que celle corrigée pour le homelab (54 → 41, un secret porte plusieurs uid : le sien et celui de chaque ownerReference). Le cluster EKS étant détruit depuis le 23/08, un décompte propre (kubectl get secrets -A --no-headers | wc -l) n'est plus rejouable. Le chiffre 55 est donc probablement surestimé, dans une proportion inconnue. Ce que ce constat établit avec certitude, c'est que le jeton d'alloy lit des secrets dans plusieurs namespaces dont fastapi-prod ; le total exact ne l'est pas.

🟠 Et le commentaire du dépôt devient lui-même un constat, même motif qu'EK-11. k8s/platform/logging/alloy-values.yaml:7 affirme : « RBAC léger (lecture des métadonnées de pods uniquement, pas de pods/log) ». C'est faux, mesuré : le rôle lit les secrets, pas seulement les métadonnées de pods. Un commentaire écrit une fois et jamais reconfronté au comportement réel du chart, sur les deux clusters.

🔴 C-U — Le contrôleur ArgoCD est administrateur du cluster homelab

Constat ouvert le 2026-08-20, campagne RBAC (bloc R).

Preuve, une seule ligne suffit :

kubectl auth can-i --list --as=system:serviceaccount:argocd:argocd-application-controller -n monitoring
Resources   Non-Resource URLs   Resource Names   Verbs
*.*         []                  []               [*]

*.* sur toutes les ressources avec le verbe * est la définition de cluster-admin. Il n'y a pas de droit au-dessus.

Ce qui rend ce constat sérieux, c'est qu'il ferme une chaîne complète. Chaque maillon a été mesuré séparément, ils se rejoignent ici :

compromission d'ArgoCD (ou d'un dépôt qu'il synchronise)
  → cluster-admin, donc create pods dans monitoring
  → monitoring est en PSA privileged, l'admission ne refuse rien (§3.5)
  → pod privilégié montant hostPath /
  → root sur le nœud
  → les trois nœuds, puis etcd, non chiffré (C-P) et non sauvegardé (C-M)

Le maillon d'entrée n'est pas le cluster, c'est le dépôt Git. ArgoCD applique ce qu'il lit. Quiconque fait entrer un manifeste dans la branche synchronisée s'exécute avec ces droits.

Nuance à écrire, sinon le constat est gonflé : un ArgoCD qui déploie des CRD, des ClusterRole et des composants de plateforme a besoin de droits très larges. cluster-admin est la valeur par défaut du chart, et la restreindre proprement demande de lister ce que les applications déploient réellement. Le défaut n'est pas d'avoir accordé beaucoup, c'est de ne pas l'avoir décidé, et de n'avoir aucun garde-fou en aval.

Recommandation. Ne pas commencer par restreindre le ClusterRole, l'exercice est long et casse au premier chart. Commencer par le garde-fou qui coûte le moins : exiger une revue humaine sur la branche synchronisée, et sortir monitoring du niveau privileged (voir §3.5). Effort : faible pour la revue, moyen pour le PSA.

Confirmé sur EKS le 2026-08-23 (bloc R, RE-5), même verdict, même chart.

argocd:argocd-application-controller           *.* : yes   créer pod : yes   suppr. pod : yes   lister secrets : yes   escalade : yes

Source du droit, capturée sur le cluster : ClusterRoleBinding/argocd-application-controller, chart argo-cd-9.5.20. Exactement le même chart qu'au homelab.

Le constat devient donc un défaut de la valeur par défaut du chart, pas un défaut d'une installation. Deux clusters, deux méthodes d'installation différentes (Ansible/Helm au homelab, ArgoCD lui-même bootstrappé côté EKS), même droit cluster-admin. Tout lecteur de ce rapport qui installe le chart ArgoCD par défaut hérite du même constat.

Portée EKS, à ne pas confondre avec le homelab. Les 8 namespaces où ce droit s'exerce sans rempart d'admission ne sont pas les mêmes (monitoring, logging, argocd, cert-manager, external-secrets, external-dns, envoy-gateway-system, tracing, voir EK-3), et c'est ce même contrôleur qui ferme le maillon 1 de la chaîne AW-11 : il peut créer le pod qui emprunte l'identité AWS du rôle ESO.

🟠 C-V — cert-manager peut créer et supprimer un pod dans n'importe quel namespace

Constat ouvert le 2026-08-20. C'est le jumeau exact de C-Q : un droit large accordé par un chart, pour un besoin réel, avec une portée que personne n'a choisie.

Preuve, la source du droit est nommée :

kubectl auth can-i create pods --as=system:serviceaccount:cert-manager:cert-manager -n monitoring
# yes   (idem metallb-system, local-path-storage, kube-bench)
ClusterRoleBinding/cert-manager-controller-challenges
  -> ClusterRole/cert-manager-controller-challenges
     resources: ["pods","services"]  verbs: ["get","list","watch","create","delete"]

Le besoin est réel. Pour prouver qu'il contrôle un domaine, cert-manager répond à un défi ACME HTTP-01 (la vérification d'un fournisseur de certificats comme Let's Encrypt) en créant un pod solver et le Service qui va devant. Il doit donc pouvoir créer un pod.

Ce que personne n'a décidé, c'est la portée. Le droit est porté par un ClusterRoleBinding, donc il vaut dans les quatorze namespaces, y compris les quatre qui sont en PSA privileged. Or les certificats du homelab ne concernent que la Gateway.

🔴 Le verbe delete mérite d'être lu deux fois. Il porte sur pods et services, partout. Une compromission de cert-manager ne donne pas seulement un pod privilégié : elle permet de supprimer le Service de la Gateway, donc de couper l'unique point d'entrée du cluster.

Recommandation. Restreindre le solver à un namespace dédié. cert-manager expose exactement ce réglage : --acme-http01-solver-... côté déploiement, et la valeur Helm correspondante. Contre-épreuve obligatoire : le même auth can-i doit répondre no sur monitoring. Effort : faible.

Confirmé sur EKS le 2026-08-23 (bloc R, RE-5), même chart, mêmes verbes :

cert-manager:cert-manager   *.* : no   créer pod : yes   suppr. pod : yes   lister secrets : yes   escalade : no

Huit ClusterRole liées, toutes du chart cert-manager-v1.16.2le même chart qu'au homelab : cert-manager-controller-{approve,certificates,certificatesigningrequests,challenges, clusterissuers,ingress-shim,issuers,orders}. challenges porte toujours create/delete sur les pods et services, c'est le maillon mesuré ici.

Comme pour C-U, c'est confirmé comme un défaut de la valeur par défaut du chart, pas d'une installation particulière. cert-manager figure aussi parmi les 16 chemins d'EK-12, sur les 8 namespaces de plateforme sans admission (EK-3).

🟡 EK-13 — Une cinquième identité peut lire tous les secrets, sans équivalent au homelab

Constat neuf, propre à EKS (RE-5), sans équivalent mesuré au homelab. La campagne RBAC du 23/08 a testé cinq identités de plateforme sur cinq droits. Quatre des cinq résultats recoupent des constats déjà connus (C-U, C-V, AW-11). Le cinquième est nouveau :

envoy-gateway-system:envoy-gateway   *.* : no   créer pod : no   suppr. pod : no   lister secrets : yes   escalade : no

envoy-gateway, le contrôleur de la passerelle d'entrée du cluster, peut lister les secrets de tous les namespaces. Il n'a ni droit de créer ou supprimer un pod, ni d'escalade : le droit est borné à la lecture des secrets, mais il n'est borné à aucun namespace.

Le besoin est réel et documenté : la Gateway API lit les Secrets Kubernetes qui portent les certificats TLS référencés par les Gateway et HTTPRoute, où qu'ils soient déclarés. C'est le même motif que C-Q (alloy) et C-V (cert-manager) : un droit large accordé par un chart, pour un besoin réel, sur une portée que personne n'a choisie namespace par namespace.

Ce qui distingue ce constat des trois autres : il n'a pas d'équivalent homelab à comparer, faute d'avoir testé cette identité côté homelab lors de la campagne du 20/08. À rejouer au homelab pour savoir si c'est un défaut du chart Envoy Gateway lui-même (généralisable) ou une particularité de la configuration EKS.

Recommandation. Restreindre la lecture des secrets par resourceNames aux certificats effectivement référencés, ou par un RoleBinding par namespace plutôt qu'un ClusterRoleBinding. Effort : moyen, la Gateway API ne propose pas nativement ce cloisonnement.

🟡 C-W — Un droit de test survit à l'identité qui l'a reçu

Constat ouvert le 2026-08-20, et c'est l'audit qui l'a fabriqué lui-même.

Une session de test du 19/08 a créé le namespace security-audit, le ServiceAccount dev-test, et un Role lui donnant create pods dans todos-demo. Le namespace a été supprimé, le RoleBinding non.

Preuve, et c'est le résultat qui surprend :

kubectl get ns security-audit
# Error from server (NotFound): namespaces "security-audit" not found

kubectl auth can-i create pods --as=system:serviceaccount:security-audit:dev-test -n todos-demo
# yes

L'autorisateur répond yes pour une identité qui n'existe pas.

Le principe à retenir : le RBAC lie des noms, pas des identités. Un RoleBinding désigne une chaîne de caractères. Il ne vérifie pas que le sujet existe, et rien ne le nettoie quand le sujet disparaît. Le droit reste armé : recréer un jour un ServiceAccount dev-test dans un namespace security-audit lui rend ce droit à la seconde où il est créé, sans que personne ne l'accorde.

Pourquoi ce constat a sa place dans le rapport bien qu'il vienne de nous. C'est la forme la plus courante du reste de chantier : un droit accordé pour un test, un incident ou une migration, et jamais retiré. Ici la trace est datée et l'auteur connu, donc le constat est vérifiable de bout en bout. Un auditeur note ses propres traces.

Recommandation, appliquée le 2026-08-20. Supprimer le Role et le RoleBinding, avec contre-épreuve. Puis, en pratique durable, ne créer les objets de test que dans un namespace jetable et supprimer le namespace et les bindings posés ailleurs. Effort : nul.

Corrigé et vérifié le 2026-08-20, c'est le seul constat du rapport clos pendant l'audit, parce qu'il a été créé par l'audit :

kubectl -n todos-demo delete rolebinding rbac-audit-pod-creator-binding   # deleted
kubectl -n todos-demo delete role rbac-audit-pod-creator                  # deleted

kubectl auth can-i create pods --as=system:serviceaccount:security-audit:dev-test -n todos-demo
# no

La contre-épreuve vaut d'être soulignée : le critère a changé de valeur. Il répondait yes avant, il répond no après. Un critère qui aurait répondu no dans les deux cas n'aurait rien prouvé du tout.

🟡 EK-10 — La CI garde un accès au cluster qu'elle n'utilise plus

Preuve, par recherche exhaustive dans la définition du pipeline :

Ce que porte gitlab-ci Encore utilisé ? Preuve
Politique IAM ecr-push oui jobs kaniko et promote-image
Access Entry Edit sur fastapi non zéro occurrence de kubectl, update-kubeconfig ou kustomize build dans .gitlab-ci.yml
Role fastapi-deploy-eso non idem
Role fastapi-deploy-httproute non idem

Pourquoi ce résidu existe. Les deux Roles ont été créés quand la CI appliquait les manifestes elle-même. Depuis le passage en GitOps, la CI pousse une image et écrit un tag dans le dépôt, et c'est ArgoCD qui déploie. Le modèle de déploiement a changé, le privilège est resté.

Portée réelle, à ne pas exagérer. Edit sur fastapi couvre les Secrets et ConfigMaps de ce namespace, mais pas les CRD Gateway. Il n'y a donc pas de prise directe sur le Gateway partagé. C'est un accès sans justification, pas une porte ouverte.

Recommandation. Retirer l'Access Entry et les deux Roles, garder la politique ecr-push. Vérifier ensuite qu'une montée complète réussit, le bootstrap Ansible posant ces Roles. Effort : faible.

Confirmé le 2026-08-23, sur le cluster monté (bloc R, RE-4). La méthode a dû changer en cours de route : la commande prévue (can-i --list --as=<arn>) est justement celle que la section 1.5.6 disqualifie pour une identité IAM. Remplacée par des questions fermées, une par droit :

<projet-eks>-gitlab-ci  fastapi       create pods : no
<projet-eks>-gitlab-ci  fastapi-dev   create pods : no
<projet-eks>-gitlab-ci  fastapi-prod  create pods : no
<projet-eks>-gitlab-ci  kube-system   create pods : no
<projet-eks>-gitlab-ci  -             cluster-admin : no

Confirmé aussi côté AWS, par la politique d'accès réellement associée (pas seulement lue dans Terraform) :

principal  arn:aws:iam::…:user/ci/<projet-eks>-gitlab-ci
policyArn  AmazonEKSEditPolicy
scope      namespace : fastapi

gitlab-ci ne peut créer aucun pod, nulle part, et n'a aucun droit d'administration. Son accès est borné à Edit sur fastapi exactement comme le lisait le dépôt, sans droit caché découvert en plus. Le constat n'est pas un privilège dangereux en soi, c'est un privilège sans usage restant : même correctement borné, le retirer réduit la surface pour rien.

Même mesure, pour situer le reste de la chaîne AW-2 : gitlab-ci-infra et youss_admin portent tous deux AmazonEKSClusterAdminPolicy, à portée cluster entière — confirmé par la même requête d'accès associée, pas seulement par le dépôt Terraform.

🔵 EK-14 — Neuf liaisons RBAC orphelines, livrées par la distribution elle-même

Constat plus faible que C-W mais plus général (RE-8). Neuf ClusterRoleBinding et RoleBinding du cluster EKS désignent un sujet qui n'existe pas :

route-controller, kube-dns, bootstrap-signer, kube-controller-manager, kube-scheduler,
cloud-provider, token-cleaner, leader-election-controller

Différence importante avec C-W : au homelab, l'orphelin avait été créé par l'audit lui-même et corrigé sur-le-champ. Ici, c'est l'état par défaut de la distribution Kubernetes et d'EKS : ces liaisons sont posées au provisionnement du cluster, avant même le premier déploiement applicatif.

Le principe reste celui de C-W : le RBAC lie des noms, pas des identités vivantes. Ces neuf droits restent armés, prêts à revenir au premier composant qui recrée un ServiceAccount portant l'un de ces noms exacts dans kube-system. Ce n'est pas exploitable directement — recréer kube-scheduler dans kube-system suppose déjà un accès élevé — mais c'est un residu qu'aucun terraform apply ni bootstrap Ansible ne nettoie, parce qu'aucun des deux ne l'a posé.

Recommandation. Aucune action côté client : ce sont des objets gérés par le plan de contrôle managé, hors du périmètre modifiable (voir §1.3.3). À signaler, pas à corriger : un lecteur qui audite un cluster EKS ne doit pas être surpris par des liaisons orphelines qui ne viennent pas de lui. Effort : nul, constat déclaratif.

🟢 Points forts du contrôle d'accès

Point Périmètre Preuve
cluster-admin n'est lié qu'à deux groupes système, aucun ServiceAccount ni compte applicatif homelab mesuré sur le cluster
Un rôle IRSA par environnement, limité au seul secret de son environnement EKS terraform/ephemeral/irsa.tf
CI cantonnée : Edit sur fastapi seul, aucun droit sur les namespaces d'environnement EKS Access Entries
automountServiceAccountToken: false sur les deux ServiceAccounts applicatifs EKS c'est la contre-mesure exacte de PT-1
Aucune clé d'accès sur le compte racine, compte racine non utilisé AWS prowler, contrôles PASS
aws-auth (l'ancien mécanisme d'accès EKS) réduit au seul rôle des nœuds : les 5 identités humaines et machine passent par les Access Entries, l'API moderne EKS RE-0, aws-auth + list-access-entries

Nuance à ne pas taire sur le homelab : 27 ClusterRoles donnent accès aux secrets. Le chiffre est identique à celui mesuré indépendamment par kubescape, deux mesures qui concordent. Mais ce n'est pas 27 anomalies : cert-manager y range ses certificats TLS, ArgoCD ses identifiants de dépôt. Le nombre brut ne veut rien dire. Seul compte le chemin depuis un point d'entrée réel, et c'est ce que mesure PT-1.

🔴 AW-9 — La clé de l'administrateur ouvre le compte entier, sans authentification forte

Le constat le plus grave du volet IAM, et il a été mesuré par accident.

Le 2026-08-19, un outil a interrogé l'API AWS avec la clé de longue durée de iamadmin, sans aucun second facteur, parce que le fichier de configuration qui portait l'instruction MFA n'était pas visible. L'appel a réussi.

Preuve aws sts get-caller-identityarn:aws:iam::…:user/iamadmin, sans MFA
Droits du compte AdministratorAccess attaché directement
Politiques inline aucune
Groupes aucun
Politiques portant aws:MultiFactorAuthPresent zéro
Âge de la clé 107 jours, jamais tournée

Le malentendu à lever, et il est structurant. Un appareil MFA existe bien sur ce compte. Mais AWS n'exige un second facteur que sur la connexion console. Sur l'API, une clé d'accès suffit, toujours, sauf si une politique IAM porte la condition aws:MultiFactorAuthPresent.

Le mfa_serial écrit dans ~/.aws/config n'est pas un réglage AWS. C'est une instruction au client, sur un poste. Elle dit à l'outil local d'appeler sts:GetSessionToken avec un code. Sur une machine sans ce fichier, l'instruction n'existe pas.

Portée réelle. Quiconque détient cette clé administre le compte, depuis n'importe quelle machine, sans second facteur. Et rien ne l'enregistrerait, CloudTrail étant absent (AW-1).

Recommandation, et le mot qui décide de tout. Une politique qui refuse les actions en l'absence de MFA, attachée à iamadmin seul.

"Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } }

🔴 BoolIfExists, pas Bool. La clé de condition prend trois états, pas deux :

Appel Valeur de aws:MultiFactorAuthPresent
session STS avec MFA true
session STS sans MFA false
clé de longue durée, en direct absente

Avec Bool, la condition ne se déclenche que si la clé existe et vaut false. Sur la troisième ligne, la clé est absente : Bool répond « condition non remplie », donc le Deny ne se déclenche pas. Elle ne bloque donc pas l'usage d'une clé de longue durée, c'est-à-dire exactement la menace de ce constat.

BoolIfExists ne fait pas la même chose qu'un Bool où l'absence vaudrait false : sa règle, documentée par AWS, est « si la clé est absente, considère la condition comme remplie, quelle que soit la valeur écrite dans la politique ». Ici la valeur écrite est false, donc absence → condition remplie → Deny déclenché. Le résultat est le bon pour cette politique précise, mais la mécanique n'est pas une substitution de valeur : une absence satisferait tout aussi bien un BoolIfExists écrit avec "true", ce qu'un vrai « absence = false » ne ferait pas.

Un seul mot sépare une politique qui protège d'une politique décorative, et les deux se relisent aussi bien.

⚠️ Deux garde-fous à l'application. Exclure du refus les actions d'enrôlement (iam:CreateVirtualMFADevice, iam:EnableMFADevice, iam:ListMFADevices, sts:GetSessionToken), sans quoi un compte sans MFA ne peut plus en ajouter un. Et prouver la politique par l'échec de l'ancien chemin : la clé brute doit rendre un AccessDenied explicite. Sans ce test négatif, c'est un document, pas un contrôle.

Effort : faible. 🔴 À appliquer après la journée live, iamadmin étant le compte qui porte Terraform et le teardown.

🔵 AW-4 — Un compte machine sans authentification forte : le sujet est ailleurs

Tranché le 2026-08-19. <projet-eks>-cockpit-panel n'a aucun mot de passe console. C'est un compte machine.

Une authentification forte ne protège qu'un accès console. Sur ce compte, elle ne protégerait rien. Le contrôle en échec est donc sans objet, et le risque réel est ailleurs : sa clé statique (EK-4).

Le constat ne disparaît pas, il se déplace. C'est la différence entre trier un rapport d'outil et le recopier.

🟡 AW-7 — Aucune politique de mot de passe, et un seul compte concerné

Tranché le 2026-08-19, et le verdict est l'inverse de ce qu'on attendait.

La politique de mot de passe IAM est un réglage unique, pour tout le compte : elle impose (ou pas) ses règles — longueur, majuscule, minuscule, chiffre, symbole, expiration, non-réutilisation — à tout mot de passe console créé sur ce compte, quel que soit l'utilisateur. Sept contrôles en échec, un par règle : ce réglage n'existe pas du tout, pour personne.

La question que le contrôle ne pose pas : un réglage absent, qui protégerait-il s'il existait ? Un mot de passe console n'a d'intérêt à encadrer que là où un mot de passe console existe. D'où la vérification manquante : aws iam get-login-profile (la commande qui dit si un utilisateur IAM a un mot de passe console), rejouée sur les cinq utilisateurs du compte.

Utilisateurs du compte Avec mot de passe console
5 1

Un seul en a un, et c'est iamadmin. Les quatre autres sont des comptes machine : ils s'authentifient uniquement par clé d'accès (API/CLI), sans mot de passe console, donc sans rien qu'une politique de mot de passe puisse encadrer. L'absence de politique est réelle pour tout le compte, mais elle n'expose concrètement qu'un seul utilisateur : iamadmin, le seul compte humain, et celui qui porte AdministratorAccess.

Ce que ça change. On aurait pu écrire « sept contrôles rouges qui ne protègent personne » et classer le constat en remplissage. C'est le contraire : la politique absente ne concerne qu'un compte, et c'est le plus privilégié du compte AWS. Un constat étroit et net vaut mieux qu'un constat large et creux.

Recommandation. Une politique de mot de passe au niveau du compte, à traiter avec AW-9 : c'est la même surface, le même utilisateur, la même session de correction. Effort : très faible.


3.4 Cloisonnement réseau

🟠 C-R — Treize namespaces sur quatorze sans aucune politique réseau

Preuve :

kubectl get netpol -A          # 6, toutes dans argocd
kubectl get cnp,ccnp -A        # 0

Les six seules NetworkPolicy du cluster sont dans argocd, et viennent du chart, pas d'une décision d'architecture. Aucune CiliumNetworkPolicy, alors que le CNI est Cilium et que c'est précisément sa fonction.

todos-demo, le namespace applicatif, n'a aucune politique. Tout pod parle à tout pod.

Ce constat tient par trois voies indépendantes, ce qui est rare et mérite d'être dit :

Voie Résultat
kubescape MITRE, C-0054 9 ressources en échec
kubescape NSA/CISA, C-0030 31 ressources en échec
PT-3 et PT-10, joués à la main sortie interne et externe libres, confirmé par contre-épreuve

Le piège de lecture, et il aurait produit un faux constat. Une des six politiques d'argocd, celle qui protège argocd-server, porte ingress: [{}]. C'est une règle vide, qui autorise tout. Elle apparaît dans kubectl get networkpolicies et ne filtre rien. Sans le test réseau, elle aurait été comptée comme une protection.

Les cinq autres sont réellement restrictives. Le chart fait donc du bon travail, seule l'interface est délibérément ouverte.

Le point favorable, mesuré par test négatif : le moteur, lui, applique bien.

Cible Résultat Attendu
argocd-redis:6379, couvert par une politique restrictive BLOQUÉ (TimeoutError) devait échouer ✅
argocd-server:80, couvert par la règle vide ATTEIGNABLE devait passer ✅

La cause est l'absence de politiques, pas un moteur défaillant. C'est la distinction à établir avant d'investir dans le chantier de cloisonnement, et elle est établie.

Détail à garder pour le diagnostic : l'échec est un TimeoutError, pas un ConnectionRefused. Le paquet est jeté en silence, signature d'un filtrage réseau.

Recommandation. default-deny ingress et egress par namespace, puis ouverture nominative. Le namespace applicatif todos-demo en premier. Effort : moyen. Suivi dans l'issue #80 du projet homelab.

🟡 EK-3 — Les namespaces de plateforme EKS n'ont ni politique réseau ni admission

Preuve, déclarative : huit namespaces de plateforme créés sans aucun label, et k8s/platform/ ne contient aucune NetworkPolicy.

Précision de décompte, vérifiée au dépouillement. L'inventaire du cluster montre en réalité neuf namespaces sans label PSA hors des quatre namespaces natifs de Kubernetes (default, kube-system, kube-public, kube-node-lease) : les huit ci-dessus, plus cilium-secrets. Ce neuvième namespace ne porte ni contrôleur ni ServiceAccount applicatif — il ne stocke que les secrets internes de Cilium — et n'a donc pas été inclus dans le périmètre de la campagne RBAC RE-2 (EK-12). L'écart est signalé plutôt que corrigé en silence : cilium-secrets reste également sans admission ni politique réseau, même s'il n'ouvre aucun chemin RBAC mesuré.

Les sept namespaces applicatifs, eux, naissent restricted et portent un default-deny-all ingress et egress.

La frontière est nette : ce qui est applicatif est cloisonné, ce qui est plateforme ne l'est pas.

Différence importante avec le homelab : ici, la moitié du cluster est bien couverte, ce qui prouve que la capacité existe et que c'est la couverture qui manque. Sur le homelab, la frontière est inverse et plus faible : c'est le namespace applicatif qui n'a rien.

Confirmé le 2026-08-23 (PT-3, détail en §4.4). Le default-deny egress de fastapi-dev bloque bien la sortie vers argocd, monitoring et external-secrets, là où le homelab laissait tout passer. La contre-épreuve sur les 15 NetworkPolicy du cluster (aucune règle vide) confirme que ce sont de vrais blocages, pas un artefact de mesure.

🟡 EK-6 — Le groupe de sécurité de la base de données s'ouvre à tout le VPC

Preuve : cidr_blocks = ["10.0.0.0/16"] en ingress sur le port 5432, au lieu du groupe de sécurité des nœuds. Le commentaire du code le reconnaît déjà : « PROD : remplacer par security_groups = [eks_node_sg_id] ».

Portée réelle. Toute machine du VPC atteint la base, pas seulement les nœuds du cluster. La NetworkPolicy allow-postgres ne protège que le trafic intra-cluster, elle ne s'applique pas à ce chemin.

Recommandation. Remplacer le CIDR par le groupe de sécurité des nœuds, comme le commentaire le prévoit déjà. Effort : très faible.

🟡 C-Y — Le trafic interne du homelab circule en clair

Preuve : les options enable-ipsec et enable-wireguard de Cilium sont vides. Aucun chiffrement du trafic entre pods.

Aggravé par un second fait mesuré : ArgoCD tourne en --insecure et est joignable en HTTP clair depuis n'importe quel pod du cluster. Le TLS s'arrête à la passerelle. Un jeton transitant par cette voie serait lisible par qui écoute le réseau du cluster.

⚠️ Piège de lecture rencontré : le Connection reset by peer observé sur le port 443 d'ArgoCD n'est pas une protection, c'est la signature du mode --insecure. À ne pas compter comme un blocage.

Le motif à retenir, et il se répète : la protection existe à l'entrée et disparaît à l'intérieur. Exactement comme pour les secrets, chiffrés dans Git et en clair dans etcd.

Recommandation. Activer enable-wireguard (le plus simple à poser sur un Cilium déjà en place, pas de certificats à gérer) pour le chiffrement est-ouest, et sortir ArgoCD du mode --insecure en posant un certificat TLS interne. Effort : moyen, le chiffrement de tout le trafic d'un cluster en production se teste avant de se généraliser.

Confirmé sur EKS le 2026-08-23 (Q12, détail en §4.4) : argocd-cmd-params-cm porte server.insecure: "true", même parti pris que le homelab. Un défaut qui se généralise plutôt qu'une particularité d'installation, comme C-U et C-V.

🟠 EK-9 — Deux consoles d'administration exposées sur Internet

Preuve, déclarative : grafana.devopsyouss.com et argocd.devopsyouss.com sont servies par le même Gateway public que les six applications.

Ce qui rend ce constat élevé et non moyen : ArgoCD a les droits d'écriture sur tout le cluster, c'est lui qui déploie. Une compromission de sa console ne donne pas un accès en lecture, elle donne le cluster.

Aucune restriction réseau devant : le Gateway n'a pas de filtre par adresse source, et k8s/platform/ ne contient aucune NetworkPolicy (EK-3). La seule barrière est l'authentification applicative.

L'hypothèse est favorable, et elle reste une hypothèse. Sur le homelab, PT-4 a montré que Grafana, MinIO et ArgoCD tiennent tous sans identifiants. Mais le homelab n'est pas sur Internet (section 2.1.3), donc ce test n'a jamais été joué depuis l'extérieur.

Joué le 2026-08-23 (PT-4, détail en §4.4), depuis Internet, sans aucun identifiant. Les deux consoles répondent 401, à l'exception de /api/v1/session/userinfo (200, corps {} — même piège qu'au homelab, un endpoint public par conception qui répond « personne »).

L'authentification applicative tient, dans des conditions plus dures que le test du homelab (joué depuis l'intérieur du cluster). Le constat lui-même ne change pas : EK-9 reste une exposition, faute de filtre par adresse source. Ce que le test change, c'est le statut de la barrière restante : vérifiée dans les pires conditions, pas seulement supposée favorable.


3.5 Admission et durcissement des charges

🟢 Le PSA du homelab tient là où il est posé

Preuve par test négatif, et le critère discrimine parfaitement. Le même pod privilégié, montant la racine du nœud en hostPath, est soumis à deux namespaces :

Cas Namespace PSA Verdict
1 todos-demo restricted REFUSÉ : privileged, hostPath, runAsNonRoot, seccomp, capabilities
2 monitoring privileged ACCEPTÉ, le pod privilégié existe

⚠️ Piège de lecture : le cas 2 émet un Warning: would violate PodSecurity "baseline" et crée quand même le pod. Un avertissement ressemble à un refus dans le flot de sortie. Il n'en est pas un. Le pod a été supprimé immédiatement, sans y exécuter la moindre commande.

La frontière du durcissement ne suit pas la ligne applicatif / plateforme comme le réseau. Elle suit le niveau PSA. Les quatre namespaces privileged (monitoring, metallb-system, kube-bench, local-path-storage) laissent passer un pod d'évasion.

🟡 C-X — Le namespace kube-system du homelab n'a aucun label d'admission

Huit namespaces sont en restricted, minio est en baseline, et kube-system n'a rien.

C'est une décision documentée du projet, pas un oubli, et elle est défendable : étiqueter kube-system en restricted casse le CNI. Elle a néanmoins une conséquence, qui est le constat.

Recommandation. Écrire la décision dans le rapport de posture du projet plutôt que dans un commentaire, pour qu'un lecteur externe ne la prenne pas pour un oubli. Effort : très faible.

🟡 C-T — Un collecteur de traces tourne en root sans en avoir besoin

Constat ouvert le 2026-08-19, au tri des ressources de C-0013.

Preuve, mesurée à l'exécution et non lue dans un manifeste :

kubectl -n monitoring exec deploy/alloy-traces -c alloy -- id -u
# 0

Pourquoi ce cas se distingue des neuf autres composants qui tournent en root. Cilium, MetalLB, kube-bench et local-path-provisioner ont besoin de l'hôte : ils programment le réseau, lisent /etc ou créent des répertoires sur le disque du nœud. Leur privilège est la surface irréductible d'un cluster auto-hébergé (section 3.9).

alloy-traces n'est pas dans ce cas. C'est un Deployment, pas un DaemonSet. Il reçoit des traces par le réseau. Il ne monte aucun hostPath, il n'a rien à lire sur l'hôte.

Ce qui rend le constat solide, c'est son propre voisin de pod. Le conteneur config-reloader du même pod, sans aucune contrainte lui non plus, tourne en uid 65534. Le durcissement était donc possible sans rien changer d'autre. Il n'a simplement pas été fait.

Impact. Limité, et il faut le dire : alloy-traces est dans monitoring, namespace PSA privileged, donc rien ne l'aurait refusé de toute façon. Le risque est un processus root de plus sur un nœud, pas un chemin d'attaque nouveau.

Recommandation. runAsNonRoot: true sur le conteneur alloy, en valeur de chart. Effort : très faible.

🟢 EK-8 — Les points forts du durcissement EKS

Un rapport qui ne liste que des manques n'est pas crédible. Ceux-ci sont vérifiables dans le dépôt et devront être reconfirmés en vol.

Point Preuve
automountServiceAccountToken: false sur les deux ServiceAccounts applicatifs k8s/base/serviceaccount.yaml, k8s/frontend/serviceaccount.yaml
PSA restricted sur les 7 namespaces applicatifs, posé à la naissance du namespace ansible/bootstrap.yml
runAsNonRoot, readOnlyRootFilesystem, capabilities: drop [ALL], seccompProfile k8s/base/deployment.yaml
default-deny-all ingress et egress dans les namespaces applicatifs k8s/base/networkpolicy-default-deny.yaml
IMDSv2 obligatoire sur les nœuds, hop_limit = 2 modules/eks/main.tf
ECR à tags immuables, scan au push, chiffré modules/ecr/main.tf

L'IMDS (Instance Metadata Service) est le service qui délivre à une machine EC2 ses identifiants temporaires. Sa version 2 exige un jeton obtenu par une requête PUT, ce qui bloque le vol par simple requête GET depuis un pod. C'est la contre-mesure exacte du scénario PT-6.


3.6 Ressources et disponibilité

🟠 C-M — Le cluster homelab n'est pas restaurable

Preuve, par recherche exhaustive dans le dépôt :

Recherche Résultat
etcdctl snapshot save dans tout le dépôt aucune occurrence
CronJob, Job ou playbook de sauvegarde etcd aucun
Nœuds de plan de contrôle déclarés un seul, k8s-cp-1

Une hypothèse a été écartée par la mesure, et elle mérite d'être écrite parce qu'elle était séduisante. Une clé AES-256 existe bien pour les sauvegardes du NAS. Elle protège le dossier docker/minio, qui contient le state Terraform, pas etcd. L'hypothèse confondait deux sauvegardes différentes.

Impact :

Perte de la VM k8s-cp-1 le cluster n'est pas restaurable
Ce qui subsisterait les manifestes du dépôt, rejouables par ArgoCD
Ce qui serait perdu l'état non déclaré : SealedSecrets déchiffrés, volumes persistants, historique

Criticité assumée, et voici pourquoi elle n'est pas critique. Sur un homelab d'apprentissage, un cluster non restaurable est un choix défendable : la reconstruction complète est justement l'exercice, et le GitOps existe pour ça. Un audit qui classe ce point critique sans lire l'intention se disqualifie.

Ce qui doit être écrit sans nuance, en revanche : la restauration n'a jamais été testée, puisqu'il n'y a rien à restaurer. La question n'est donc pas « la sauvegarde est-elle chiffrée », c'est « que se passe-t-il si k8s-cp-1 meurt », et cette question n'a aucune réponse écrite.

Recommandation. Un etcdctl snapshot save planifié vers MinIO, plus un test de restauration joué au moins une fois. Sans ce test, la sauvegarde est un garde-fou documentaire. Effort : moyen.

🟠 C-S — La plateforme tourne sans limite mémoire, au-dessus d'un plan de contrôle unique

Preuve, prise sur le cluster :

kubectl get pods -A -o json | jq -r '.items[] | .metadata.namespace as $ns
  | .spec.containers[] | select(.resources.limits.memory == null) | $ns' | sort | uniq -c | sort -rn

73 conteneurs sans limite mémoire :

Namespace Conteneurs sans limite
monitoring 22
metallb-system 20
kube-system 16
argocd 7
cert-manager 3
envoy-gateway-system 2
sealed-secrets, minio, local-path-storage 1 chacun

Le point favorable, à écrire aussi. todos-demo, le namespace applicatif, a bien ses limites mémoire : 512Mi pour le backend, 128Mi pour le frontend. Le défaut est entièrement côté plateforme.

Pourquoi c'est un constat composé, et pas une simple non-conformité. Trois faits se cumulent :

  • 16 conteneurs de kube-system sans limite mémoire, sur le nœud qui héberge le plan de contrôle
  • C-M : un seul plan de contrôle, aucune sauvegarde d'etcd
  • aucune LimitRange ni ResourceQuota nulle part pour poser un plafond par défaut

Un composant de plateforme qui fuit peut saturer k8s-cp-1, et le cluster n'est pas restaurable. Le risque ne vient pas de la limite manquante seule, il vient de sa combinaison avec l'absence de sauvegarde.

Le plus gros consommateur mémoire du cluster est l'API server lui-même. Mesuré le 2026-08-19 :

Rang Conteneur Namespace Mémoire en service Limite posée ?
1 kube-apiserver kube-system 1,23 Go non
2 prometheus monitoring 437 Mo non
3 grafana monitoring 433 Mo non
4-6 cilium-agent ×3 kube-system ~335 Mo chacun non
7-8 alloy ×2 monitoring ~305 Mo chacun non
9 argocd-application-controller argocd 299 Mo non

C'est la formulation la plus juste du constat : le plus gros consommateur de tout le cluster est le composant dont dépend le cluster entier, il n'a pas de plafond, et il tourne sur la seule machine qui n'est pas sauvegardée.

État actuel, et le dire évite de gonfler le constat. Mesuré dans Prometheus, pas par kubectl top :

Nœud Adresse Occupation mémoire
k8s-cp-1, plan de contrôle 192.168.1.50 27,8 %
k8s-worker-1 192.168.1.51 13,0 %
k8s-worker-2 192.168.1.52 13,7 %
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)

⚠️ Deux instances scrapées par le même Prometheus ne sont pas des nœuds du cluster : le Proxmox (192.168.1.250) et le NAS (192.168.1.37), sous le job infra-node-exporter. Les inclure dans ce constat serait une erreur de périmètre.

Aucun conteneur tué pour dépassement mémoire en 30 jours. Le risque est réel mais non réalisé, et cette phrase repose sur un critère qui discrimine :

sum(increase(container_oom_events_total[30d]))     # rend 0
count_over_time(container_oom_events_total[30d])   # rend 1244 séries

📌 Le doublage n'est pas une précaution, c'est le test. Un 0 seul est ambigu : il vaut « aucun incident » si la métrique est collectée, et « personne ne mesure » si elle ne l'est pas. Les 1244 séries prouvent qu'elle est collectée sur 1244 conteneurs, donc que le zéro est un vrai zéro.

Ce que ce chiffrage remplace. Une version antérieure de ce constat annonçait 40 %, 29 % et 22 %, mesurés par kubectl top, avec la mention « ordre de grandeur uniquement, à confirmer ». Un rapport d'audit ne publie pas un chiffre dont il écrit lui-même qu'il n'est pas fiable.

Recommandation. Une LimitRange par namespace de plateforme pose un plafond par défaut sans toucher aux charts. C'est le geste qui protège l'unique plan de contrôle. Effort : faible.

C-S (suite) — Aucun quota ni LimitRange, la preuve par la capacité plutôt que par la mémoire

Même cause que C-S ci-dessus, vue sous un autre angle : pas un second constat, la confirmation du même par un chemin différent (capacité CPU plutôt que limites mémoire).

kubectl get resourcequota,limitrange -A   # No resources found

Constat structurel, mesuré avant tout test. Vérifié par le scénario PT-9 : un pod demandant 8 CPU et 16 Gi est admis, puis reste Pending faute de capacité. Ce qui le borne est la capacité des nœuds, pas une politique.

Nuance importante à écrire : un pod qui ne demande rien serait admis et planifié. Sans LimitRange pour injecter des valeurs par défaut, rien ne l'empêcherait de consommer tout ce qu'il trouve.

🔵 EK-7 — La base de données EKS n'a ni sauvegarde ni protection contre la suppression

Preuve, déclarative : backup_retention_period = 0, deletion_protection = false, skip_final_snapshot = true.

Choix assumé et cohérent avec une infrastructure détruite chaque soir, le schéma étant reconstruit par Alembic à chaque montée. À écrire comme arbitrage documenté, pas comme défaut.

Une réserve, et une seule : le terme « environnement de production » est employé pour le namespace fastapi-prod. Une production sans sauvegarde n'est pas une production. Le vocabulaire mérite d'être aligné sur la réalité, ou la réalité sur le vocabulaire.

Confirmé et complété le 2026-08-23 (diff CIS, §4.6) : rds_instance_multi_az échoue aussi. Une seule instance, aucune réplique dans une seconde zone de disponibilité. Même famille de choix que backup_retention_period = 0 : cohérent avec un schéma reconstruit à chaque montée, la même réserve sur le vocabulaire de « production » s'applique.


3.7 Chaîne d'intégration continue et dépôts

🟡 C-L — Le dépôt cockpit pouvait être poussé vers un dépôt public

État à la date du constat, 2026-08-17. Le dépôt cockpit est privé, ce qui est correct. Mais il portait un second remote, glab-base, dont l'URL de push visait un dépôt public. Un git push glab-base main aurait publié tout le cockpit.

Vérifié avant toute action, et c'est ce qui a évité un constat exagéré : les deux dépôts n'ont aucun ancêtre commun, leurs racines n'ont aucun fichier en commun, et le journal local ne porte aucun push vers ce remote. Pousser d'ici n'aurait pas complété le projet distant, cela l'aurait remplacé, et Git l'aurait refusé sans --force. Ce remote ne servait donc qu'à lire.

🔴 Seule exception accordée à la règle « on collecte, on ne répare pas », sur demande explicite, et elle est datée ici :

git remote set-url --push glab-base DISABLED

Contre-épreuve passée : push refusé, fetch toujours fonctionnel, origin intact.

⚠️ La configuration Git est locale et non versionnée. La correction ne vaut que sur cette machine. À rejouer sur la seconde machine de travail, où le même remote existe probablement avec son push actif.

🔵 EK-11 — Un commentaire du dépôt affirme le contraire du dépôt

Preuve : le bloc ALLOWED_ORIGINS d'un overlay Kustomize écrit que le host frontend « n'existe pas encore, le frontend restant non exposé ».

Faux au commit de gel. Le frontend a une HTTPRoute active, et les trois overlays lui donnent app-dev, app-staging et app.devopsyouss.com.

Constat mineur en soi, et il est ici pour une autre raison. Il a produit une erreur d'inventaire dans la première version de cet audit, corrigée par relecture. C'est la démonstration en direct, datée, de ce qu'un commentaire écrit une fois et jamais reconfronté au code fait au lecteur suivant.

Recommandation. Corriger le commentaire. Effort : très faible.

🔵 EK-15 — Qui peut faire entrer un manifeste dans la branche synchronisée

Mesuré le 2026-08-26. Postérieur au gel du 18/08 : conformément à §1, cette mesure porte sa propre date.

C-U (🔴, §3.3) désigne le maillon d'entrée de sa chaîne : « quiconque fait entrer un manifeste dans la branche synchronisée s'exécute avec cluster-admin ». Le rapport ne dit nulle part qui est ce « quiconque ». Un maillon nommé mais jamais mesuré ne se corrige pas. Voici la mesure.

Preuve :

EKS <repo-eks> homelab <repo-homelab>
Visibilité publique publique
Membres du projet 2 : <owner> (Owner), <bot-renovate> (Developer) 2 : <owner> (Owner), <bot-renovate> (Developer)
Branche suivie par ArgoCD develop (targetRevision, 8 Applications) main (réglage GitLab, non reconfronté au dépôt)
Push sur cette branche Maintainers Maintainers
Push sur main No one (c'est la branche suivie)
Fusion conditionnée au pipeline only_allow_merge_if_pipeline_succeeds ✅ idem

🟢 La porte extérieure est fermée, et il faut l'écrire. Sur GitLab, une visibilité public donne la lecture et le clonage, jamais l'écriture : celle-ci exige un rôle dans le projet. Deux comptes en ont un, dont un bot de mise à jour de dépendances. Le seul chemin ouvert à un inconnu est le fork suivi d'une demande de fusion, qui ne devient un déploiement que si un Maintainer la fusionne. Ce n'est pas un accès, c'est une proposition soumise à décision.

🔵 La porte intérieure, elle, n'a pas de serrure. only_allow_merge_if_pipeline_succeeds ne gouverne que les fusions. Un git push direct sur la branche synchronisée, autorisé aux Maintainers sur les deux dépôts, ne passe par aucune demande de fusion : donc par aucune revue, et par aucune condition de pipeline. Le garde-fou en place ne couvre pas le chemin réellement emprunté au quotidien.

⚠️ Le corollaire de PT-8, en miroir, et il est plus dur que PT-8 lui-même. PT-8 mesure qu'ArgoCD n'éjecte pas un objet qu'il ne connaît pas. Par cette porte-ci, l'effet s'inverse : une ressource entrée par Git est connue, donc déployée, et selfHeal la recrée à chaque suppression. La persistance devient le comportement nominal de l'outil. On ne s'en défait qu'en corrigeant le dépôt, jamais le cluster.

Ce que ça change pour C-U. Sa recommandation (« exiger une revue humaine sur la branche synchronisée », effort faible) cesse d'être une précaution de principe : c'est le seul contrôle absent sur ce chemin, tous les autres étant déjà en place et mesurés ci-dessus.

Recommandation. Passer le droit de push sur les branches synchronisées à No one (develop côté EKS, main côté homelab), comme main l'est déjà côté EKS. Tout changement passe alors par une demande de fusion et hérite de la condition de pipeline déjà active. C'est un réglage, pas un chantier, et il ne remplace pas la revue humaine de l'action 31 : il la rend inévitable. Effort : très faible.

Limite de cette mesure, à ne pas surinterpréter. Côté EKS, la branche suivie est vérifiée dans le dépôt (targetRevision: develop, 8 Applications). Côté homelab, seuls les réglages GitLab ont été mesurés : le dépôt n'était pas disponible sur le poste au moment de cette vérification, et sa branche synchronisée n'a donc pas été reconfrontée à ses fichiers.

🟠 EK-16 — Les clés du compte AWS sont servies aux pipelines de toutes les branches

Mesuré le 2026-08-26, postérieur au gel : la mesure porte sa date. Métadonnées des variables seules, aucune valeur n'a été lue. Constat né d'une relecture externe qui a désigné le maillon d'entrée d'AW-2 comme jamais mesuré.

Une variable CI « protégée » n'est servie qu'aux pipelines des branches protégées. Non protégée, elle est servie au pipeline de n'importe quelle branche. « Masquée » est autre chose : cela cache la valeur dans les journaux de job, et cela ne restreint aucun accès.

Preuve, projet EKS, 16 variables. Les six qui portent un secret d'accès :

Variable Masquée Protégée
AWS_ACCESS_KEY_ID 🔴 non
AWS_SECRET_ACCESS_KEY 🔴 non
AWS_INFRA_ACCESS_KEY_ID 🔴 non
AWS_INFRA_SECRET_ACCESS_KEY 🔴 non
TF_VAR_db_password 🔴 non
SECRET_KEY 🔴 non
AWS_DEFAULT_REGION (pour comparaison) non oui

🔴 La dernière ligne est le constat. AWS_DEFAULT_REGION, qui n'est pas un secret, est protégée. AWS_SECRET_ACCESS_KEY, qui ouvre le compte, ne l'est pas. La protection a été posée à l'inverse de la sensibilité.

Ce que ça ouvre. Quiconque peut pousser une branche dans ce dépôt peut y déposer un job qui lit ces clés. Le masquage ne s'y oppose pas : il ne protège que l'affichage, et se contourne en encodant la valeur avant de l'imprimer. C'est le maillon 1 d'AW-2, jusqu'ici supposé et non mesuré. Couplé à EK-15 (le push direct sur la branche synchronisée n'exige ni revue ni pipeline vert), la chaîne du constat le plus grave de ce rapport est désormais mesurée de bout en bout.

📌 Et ce « quiconque » a un nom, EK-15 l'a mesuré. Le dépôt compte deux membres : <owner> (Owner) et <bot-renovate> (Developer). Sur GitLab, un Developer pousse librement sur toute branche non protégée et déclenche son pipeline, lequel reçoit précisément les variables non protégées. Le maillon d'entrée d'AW-2 est donc le jeton du bot de mise à jour de dépendances, un compte machine que personne ne surveille et dont le jeton vit hors du dépôt. La visibilité publique du projet n'élargit pas ce maillon : le pipeline d'une demande de fusion venue d'un fork tourne sur le projet du contributeur, pas sur celui-ci, et n'y reçoit donc aucune variable. Réglage par défaut de GitLab, non reconfronté à la configuration du projet : c'est la seule affirmation de ce constat qui ne repose pas sur une mesure.

⚠️ Et le correctif évident est faux, ce qui est le vrai enseignement. Cocher « protégée » sur ces six variables casserait les pipelines de demandes de fusion : une MR tourne sur sa branche source, qui n'est pas protégée, donc terraform plan et les jobs d'image perdraient leurs identifiants. Ce n'est pas une conjecture, le projet l'a déjà payé : le 2026-06-10, sur la MR !137, build-candidate a échoué faute de variables protégées disponibles pendant la fenêtre de déprotection.

🟢 Le homelab a tranché cette question, et c'est ce qui les sépare. Ses 6 variables sont toutes protégées, au prix d'une décision écrite avant leur création : jobs d'infrastructure sur la branche protégée, déclenchés à la main. Quatre n'y sont pas masquées, dont deux (MINIO_CA_BUNDLE, TFVARS) sont multi-lignes, que GitLab ne sait pas masquer : contrainte de l'outil, pas négligence. Les deux autres sont des identifiants, pas des secrets.

📌 Le constat n'est donc pas « EKS a mal réglé », c'est « un projet a décidé, l'autre a hérité du défaut ». Même motif que C-Q, où le droit de lecture sur tous les secrets venait du chart Helm et que personne n'avait choisi. Un réglage par défaut n'est pas une décision, et la différence se voit quand on demande qui l'a prise.

Recommandation, et elle n'est pas « cocher la case ». Trois voies, par ordre de solidité :

  1. Supprimer la clé statique par la fédération OIDC (action 34). Le pipeline reçoit alors des identifiants courts, délivrés selon la branche. La question « protégée ou non » disparaît avec la clé. C'est le seul correctif qui supprime la cause.
  2. Séparer les privilèges : une clé restreinte, non protégée, pour ce dont les MR ont réellement besoin (lecture, plan) ; une clé privilégiée, protégée, pour le déploiement depuis la branche protégée. Le pipeline de MR continue de tourner, sans porter le compte entier.
  3. La voie du homelab : jobs qui ont besoin du compte réservés à la branche protégée et déclenchés à la main. Elle fonctionne, et elle coûte du confort.

Effort : faible pour la voie 2, élevé pour la voie 1.

🟠 EK-17 — La version du plan de contrôle est épinglée une fois, et surveillée par personne

Mesuré le 2026-08-27, postérieur au gel : la mesure porte sa date. Constat né d'un signal extérieur au projet, un courriel du fournisseur, ce qui fait déjà partie du constat.

Le cluster EKS tourne en 1.32. Le cluster homelab, auquel la section 5 le compare, tourne en v1.36.2. Le rapport a décrit le second avec sa version et le premier sans, jusqu'à cette relecture. Quatre versions mineures d'écart, jamais écrites.

Preuve :

Quoi Mesure
Version épinglée terraform/ephemeral/variables.tf:29, eks_cluster_version default = "1.32"
Surcharge éventuelle aucune, aucun .tfvars ne redéfinit cette variable
Date de l'épinglage 2026-05-12, commit aa7f3fd
Modifications depuis aucune, soit trois mois et demi
Managers du bot de mise à jour pip_requirements, dockerfile, gitlabci, kustomize (renovate.json)
Manager terraform non activé
Origine du signal courriel du fournisseur, 2026-08-27

🔴 Le constat n'est pas « une vieille version », c'est « personne ne pose la question ». Un choix de version fait une fois est normal. Ce qui ne l'est pas, c'est qu'aucun mécanisme du projet ne le réexamine : ni la CI, ni un contrôle CIS, ni le bot de mise à jour. Le projet a appris la péremption de son plan de contrôle par son fournisseur, trois mois et demi plus tard.

⚠️ Et le bot donne une couverture qu'il n'a pas. Renovate tourne sur ce dépôt et ouvre des demandes de fusion sur les images, les dépendances Python, la CI et les manifests. Sa présence suggère que « les versions sont suivies ». Elles le sont, sauf celle du plan de contrôle : le manager terraform n'est pas activé, et même activé il suit les versions de modules et de providers, pas la valeur default d'une variable. Cette version est hors de portée de tout bot, par construction. Même famille que C-Q et EK-16 : un défaut que personne n'a choisi.

📌 Le modèle éphémère masque le problème au lieu de l'exposer, et c'est le point le plus intéressant. Un cluster détruit puis remonté depuis Terraform ne subit jamais de montée de version : il naît directement dans la version épinglée. Le geste réel — monter le plan de contrôle, puis faire tourner les groupes de nœuds pendant que l'application sert — n'est donc jamais joué, et son absence ne se voit nulle part. Le caractère éphémère est un choix de coût assumé (§1.3), pas un défaut ; mais il retire l'occasion d'exercer un geste que la production impose, et cette conséquence n'était écrite nulle part avant ce constat.

Impact. Trois effets distincts, à ne pas confondre :

  1. Correctifs : une version en fin de support standard ne reçoit plus de correctifs au même rythme. C'est de la gestion des correctifs, domaine couvert par le CIS Benchmark.
  2. Coût : le support étendu d'EKS facture le plan de contrôle plus cher. Aucun frais ne court au moment de ce constat, le cluster étant détruit depuis le 23/08. L'effet se déclenche au prochain montage.
  3. Comparabilité de la section 5 : deux clusters séparés de quatre versions mineures ne sont pas comparés dans les mêmes conditions. Voir §1.3.8.

Recommandation. Deux gestes, et le second compte plus que le premier.

Monter la version, et poser le mécanisme qui reposera la question. Une montée sans mécanisme reproduit le constat dans six mois : c'est le motif de la fiche garde-fou-documentaire-vs-controle. Le mécanisme peut être l'activation du manager terraform avec une règle explicite sur cette variable, un contrôle de CI qui compare la version épinglée au catalogue AWS, ou une échéance de revue écrite. Ce qui compte est qu'il soit exécuté, pas écrit.

Et jouer la montée en place au moins une fois, sur un cluster qui tourne, plutôt que de la contourner par une reconstruction. Détruire puis remonter en version cible est une installation neuve : elle ne prouve rien du processus. Détail en action 38. Effort : moyen.

🟢 La chaîne de sécurité des deux projets est déjà outillée

À écrire pour ce qu'elle vaut : Trivy, tfsec, kube-linter, gitleaks, semgrep et Renovate tournent déjà dans les pipelines des deux projets. L'audit les a réutilisés, il n'a pas eu à les introduire.


3.8 Poste d'administration

Cette section n'existait pas dans le plan initial du rapport. Elle mérite d'exister, et c'est elle-même un constat : les deux clusters ont été audités, la machine qui les administre ne l'a jamais été.

🟠 C-N — Le poste concentre les clés des deux infrastructures

Mesuré le 18/08, sans lire aucun contenu.

Élément Emplacement Permissions Système de fichiers
Clé privée SealedSecrets, qui déchiffre tous les secrets du homelab ~/.ssh/ 700 dossier, 600 fichiers ext4 réel
Clé AES-256 des sauvegardes du NAS ~/.ssh/ 600 ext4 réel
Identifiants AWS ~/.aws//mnt/c/... 777 montage Windows 9p

Le point favorable d'abord, il est réel. ~/.ssh est sur le disque Linux, en 700, fichiers en 600. Les permissions s'appliquent vraiment. C'est exactement ce que recommande l'ADR 009 du projet homelab : la clé de chiffrement vit hors du système qu'elle protège.

Le constat transverse, qui n'appartient à aucun des deux clusters. Une seule machine porte la clé qui déchiffre tous les secrets du homelab, la clé qui déchiffre ses sauvegardes, et les identifiants du compte AWS. Le poste d'administration est le point de compromission unique des deux infrastructures. Et c'est aussi celui dont la posture est la moins mesurée : ni scanné, ni durci par un rôle Ansible, ni couvert par un référentiel.

🟡 C-G — Un emplacement où les permissions n'ont aucun effet

Preuve comparative, même commande, deux emplacements :

Emplacement Système de fichiers Après chmod 600
~/.aws disque Windows en 9p / drvfs 777
dépôt sur ext4 natif ext4 600

Le chmod 600 a été accepté sans erreur et n'a rien appliqué. Tous les fichiers de ~/.aws étaient en -rwxrwxrwx, dont credentials et les fichiers de session.

Portée exacte, à ne pas exagérer. Le défaut était circonscrit à ~/.aws. Tous les autres emplacements sensibles (~/.ssh, ~/.awsvault, ~/.mc, ~/.kube) sont sur ext4 et en 600.

Addendum du 2026-08-18 : corrigé, et le verdict change

Le constat ci-dessus reste tel quel, il était exact à sa date. Cet addendum dit ce que la correction a mesuré en plus. Il ne contrevient pas à la règle de non-réparation : le constat était déjà écrit avant d'être réparé.

Correction appliquée. ~/.aws est maintenant un vrai dossier sur ext4, en 700, fichiers en 600. Les six fichiers ont été copiés, vérifiés identiques par diff, puis supprimés du disque Windows.

Ce que la vérification a révélé, et qui n'était pas dans le constat initial. Le constat mesurait les permissions sans lire les contenus. En les lisant :

Élément Mesure État
Clé statique du profil [default] aws sts get-caller-identityInvalidClientTokenId déjà invalide
Trois fichiers de session temporaire dates d'expiration lues dans les fichiers tous expirés
Caches sso/ et login/ plus récente expiration au 2026-05-20 expirés

L'échec de la clé [default] a été reproduit sur le fichier Windows d'origine avant suppression. La clé était donc morte avant le déplacement, ce n'est pas la manipulation qui l'a cassée.

📌 Le piège de méthode, à retenir : aws configure list-profiles prouve seulement que le fichier config est lu. Il ne teste jamais le fichier credentials. Il fallait un appel réel. C'est un critère qui ne discrimine pas.

Ce que ça change au verdict. Ce n'était pas une fuite active. Aucun secret AWS vivant ne se trouvait sur le disque Windows au moment de la mesure. Le défaut est structurel : un emplacement incapable de porter des permissions, pas un secret exposé.

Ce que ça ne change pas, et c'est le point fort du constat. La journée live EKS aurait réécrit les deux fichiers de session avec des jetons vivants, à cet endroit. Le constat ne décrivait pas un risque passé, il décrivait un risque programmé à sept jours. C'est la formulation à retenir : un défaut latent que le calendrier allait armer.

Reste à vérifier : la clé statique est morte côté client, mais on ignore si elle existe encore dans IAM. Si elle apparaît, même en Inactive, elle doit être supprimée.


3.9 Ce qui a été écarté, et pourquoi

Un audit qui garde tout ce que ses outils produisent n'est pas un audit. Six familles de résultats ont été rejetées après vérification. Cette section est ce qui distingue un rapport d'un export.

❌ « Le compte racine n'a pas d'authentification forte » — faux

iam_root_hardware_mfa_enabled est en échec. Mais iam_root_mfa_enabled est PASS : le compte racine a une MFA virtuelle, et le CIS exige une MFA matérielle. L'écart est mineur.

Renforts de contexte, tous PASS : aucune clé d'accès sur le compte racine, compte racine non utilisé.

Recopier le titre aurait produit un constat faux qui décrédibilisait tout le rapport.

❌ « Cinq comptes sans authentification forte » — à qualifier, pas à écrire

Cinq échecs sur iam_user_hardware_mfa_enabled. Mais quatre sont des comptes machine sans accès console, et iam_user_mfa_enabled_console_access est PASS pour les cinq.

Le vrai risque IAM de ce compte n'est pas la MFA, ce sont les identifiants de longue durée (EK-4, AW-3).

C-0012 « Applications credentials in configuration files » — faux positif intégral

Quatre ressources signalées, aucune ne contient d'identifiant. kubescape se déclenche sur le nom de la clé, pas sur son contenu.

Ressource Ce qui a déclenché Ce que c'est réellement
argocd/argocd-cm application.instanceLabelKey le nom d'un label
kube-system/cilium-config hubble-tls-key-file un chemin vers un fichier
kube-system/cilium-config mesh-auth-* des paramètres de configuration
metallb-system/metallb-controller METALLB_ML_SECRET_NAME le nom d'un Secret

Deuxième occurrence du même piège, sur un outil différent. Un contrôle qui compare un nom de clé produit du faux.

C-0068 « PSP enabled » — contrôle obsolète, et il dit l'inverse de la réalité

PodSecurityPolicy est supprimé de Kubernetes depuis la 1.25. Le cluster est en 1.36. Le contrôle ne peut structurellement pas passer.

Pire, il induit en erreur : son remplaçant, PSA, est bien en place, huit namespaces en restricted, et PT-5 l'a prouvé en étant refusé. Retenir ce contrôle aurait produit un constat exactement contraire à la réalité mesurée.

📌 Avant d'écrire un constat, vérifier que le contrôle s'applique encore à la version installée.

C-0013 « conteneurs non-root » — 12 des 23 ressources sont des faux positifs

Écrire « 23 conteneurs tournent en root » serait faux. Le tri complet, fait le 2026-08-19 sur le cluster en marche, donne quatre catégories et non une.

Catégorie Ressources Verdict
Protégées par runAsNonRoot: true argocd ×7, cert-manager ×3, metallb-controller (uid 65534), sealed-secrets-controller (uid 1001) 12 faux positifs
Root nécessaire, surface irréductible cilium, cilium-envoy, cilium-operator, metallb-frr-k8s, metallb-speaker, metallb-frr-k8s-statuscleaner, kube-bench ×2, local-path-provisioner ⚪ 9, légitimes
Root explicite monitoring/DaemonSet/alloy, runAsUser: 0 🔴 voir C-Q
Root réel, non justifié monitoring/Deployment/alloy-traces 🔴 voir C-T

Pourquoi les 12 premières sont des faux positifs. Le contrôle échoue parce que l'UID n'est pas fixé explicitement. Mais runAsNonRoot: true suffit : le kubelet refuse de démarrer le conteneur si son image tourne en root. La protection est effective. Fixer l'UID est un durcissement supplémentaire souhaitable, pas la correction d'un conteneur root.

📌 Le piège de méthode, et il coupe dans les deux sens. Un conteneur sans contrainte dans son manifeste n'est ni root ni non-root. C'est l'image qui décide, et il faut mesurer. Deux conteneurs du même pod le prouvent :

Conteneur Contrainte au manifeste uid réel mesuré
alloy-traces / alloy aucune 0, root
alloy-traces / config-reloader aucune 65534, non-root

Même absence de contrainte, deux résultats opposés. Conclure depuis le manifeste aurait produit une erreur, dans un sens ou dans l'autre. La mesure est kubectl exec ... -- id -u.

C-0270 « limites CPU » — la recommandation est contestée, et l'absence est délibérée

37 ressources en échec, sévérité High. À ne pas retenir.

Mesuré sur todos-demo :

Conteneur requests CPU limits CPU requests mém. limits mém.
backend 100m aucune 128Mi 512Mi
frontend 10m aucune 32Mi 128Mi

Ce n'est pas un oubli, c'est une configuration cohérente. Poser une limite CPU provoque du throttling, c'est-à-dire l'étranglement du conteneur par le noyau, même quand le nœud a du CPU libre. Les conséquences sont documentées : sonde de disponibilité qui expire, puis erreurs 503.

La mémoire et le CPU ne se traitent pas pareil. Le CPU est compressible, on ralentit. La mémoire ne l'est pas, on tue le processus. D'où C-0271 retenu (→ C-S) et C-0270 écarté.

📌 L'outil les classe tous les deux High, avec des volumes voisins, 37 et 31. Un dépouillement qui ne les sépare pas produit un rapport qui recommande une mauvaise pratique.

⚪ Sept contrôles de privilège hôte, sept ressources distinctes — surface irréductible, pas défaut de configuration

⚠️ Titre et décompte corrigés à la relecture du 24/08, même bug qu'en 1.5.3 : le chiffre publié ici comptait des lignes de résultat (une par couple ressource × contrôle), pas des ressources. cilium et cilium-envoy échouent sur plusieurs des sept contrôles à la fois et étaient comptés autant de fois. En ressources distinctes, recroisé sur les résultats bruts de kubescape : 7, pas onze. Détail complet en §5.5.

Sept contrôles kubescape : C-0057 (conteneur privilégié), C-0048 et C-0045 (hostPath), C-0041 (hostNetwork), C-0046 (capabilities), C-0038 (hostPID/hostIPC), C-0044 (hostPort). Quatre d'entre eux (C-0041, C-0046, C-0038, C-0044) n'ont en réalité aucune ressource en échec sur ce cluster : la surface irréductible tient tout entière sur C-0045, C-0048 et C-0057.

Les sept ressources en échec sont toutes des composants d'infrastructure : cilium et cilium-envoy (le CNI), metallb-frr-k8s (le LoadBalancer — pas metallb-speaker, qui ne figure dans aucun résultat en échec, contrairement à ce qu'une version antérieure affirmait), node-exporter et alloy (métriques et logs), et les deux CronJobs de kube-bench, l'auditeur lui-même.

Un CNI ne peut pas fonctionner sans privilèges ni hostPath. Ce n'est pas une erreur, c'est la surface d'attaque irréductible d'un cluster auto-hébergé.

Cela explique aussi PT-5 : l'évasion a réussi dans monitoring parce que ce namespace est privileged, et il l'est parce qualloy et node-exporter ont besoin de hostPath. Le PSA permissif y est une conséquence, pas une négligence.

🔵 Troisième axe du comparatif, à ne pas trancher trop vite. Sur EKS, le cluster audité tourne aussi sous Cilium installé et opéré par le client, pas sous le CNI VPC managé par AWS : ce n'est donc pas cette surface-là que le managé retire. Ce qu'il retire réellement, c'est le répartiteur de charge (un NLB AWS, hors du cluster, à la place de MetalLB) et le plan de contrôle lui-même. Le managé ne sécurise pas la surface applicative que le client choisit d'installer (CNI compris) ; il retire seulement ce qu'il gère à sa place. Mesure complète et chiffrée en §5.5.


3.10 Table de correspondance des identifiants

À conserver jusqu'à la version finale, puis à retirer.

Ancien identifiant Nouveau Sujet
C-H AW-1 le compte AWS ne journalise rien et ne détecte rien
C-I AW-8 + section 3.9 MFA du compte racine, à qualifier
C-J AW-3 + EK-4 identifiants de longue durée
C-K AW-5 bucket du state Terraform
C-C C-O + C-P il portait deux constats en un : l'absence de journal d'audit et l'absence de chiffrement etcd. Deux causes, deux corrections, deux dates de fermeture. Un identifiant qui en couvre deux ne peut pas être suivi

3.11 Relecture OWASP Kubernetes Top 10

C'est le quatrième référentiel annoncé en §1.4, et le seul appliqué après coup. Les trois autres ont orienté la recherche : CIS et NSA/CISA disent quoi vérifier, MITRE dit quoi essayer. Celui-ci ne cherche rien. Il pose une seule question aux 48 constats déjà écrits : est-ce qu'une famille de risque connue n'apparaît nulle part dans ce rapport ?

Un référentiel appliqué avant la recherche mesure la conformité. Appliqué après, il mesure la couverture de l'audit lui-même. C'est la raison de sa place ici, en fin de section 3.

3.11.1 La version utilisée, et pourquoi elle a changé le résultat

Grille retenue : OWASP Kubernetes Top 10, version 2025. Elle remplace la version 2022, encore la plus citée dans l'outillage et les articles.

Le remplacement n'est pas cosmétique, huit codes sur dix désignent une famille différente :

Code Version 2022 Version 2025
K01 Insecure Workload Configurations Insecure Workload Configurations
K02 Supply Chain Vulnerabilities Overly Permissive Authorization Configurations
K03 Overly Permissive RBAC Secrets Management Failures
K04 Policy Enforcement Lack Of Cluster Level Policy Enforcement
K05 Inadequate Logging Missing Network Segmentation Controls
K06 Broken Authentication Overly Exposed Kubernetes Components
K07 Network Segmentation Misconfigured And Vulnerable Cluster Components
K08 Secrets Management 🆕 Cluster To Cloud Lateral Movement
K09 Misconfigured Cluster Components Broken Authentication Mechanisms
K10 Vulnerable Components Inadequate Logging And Monitoring

⚠️ Un code K0x cité sans son millésime ne veut rien dire. K08 désignait la gestion des secrets en 2022, il désigne le mouvement latéral vers le cloud en 2025. Tous les codes de cette section sont ceux de 2025.

Deux changements portent directement sur ce rapport, et la comptabilité des dix familles se referme ainsi : deux familles neuves (K06, K08), une disparue (Supply Chain), deux fusionnées (les K09 et K10 de 2022 deviennent le seul K07 de 2025), et six qui changent de rang. Au total, huit codes sur dix désignent une famille différente entre les deux versions, seuls K01 et K04 restent stables.

🆕 K08 « Cluster To Cloud Lateral Movement » est une famille neuve en 2025, et elle décrit exactement AW-2, deuxième des six risques majeurs de ce rapport (§0) : un pod compromis qui pivote vers le compte cloud. OWASP nomme trois vecteurs, dont deux ont été mesurés ici :

Vecteur nommé par OWASP Mesuré dans ce rapport Résultat
Static Cloud Credentials AW-3, EK-4, EK-16 🔴 cinq clés statiques actives, aucune rotation
Unrestricted Metadata Service Access EK-8, contre-épreuve PT-6 (§4.3) 🟢 IMDSv2 obligatoire, hop_limit = 2, blocage prouvé
Overly Permissive Node Roles non mesuré le rôle IAM des nœuds n'est audité nulle part

⚠️ Le troisième vecteur est un trou, et il faut le dire ici plutôt que le laisser deviner. Ce rapport mesure des rôles d'identité de charge applicative trop larges (AW-11, le rôle du contrôleur ESO qui traverse le cloisonnement par environnement), ce qui n'est pas la même chose qu'un rôle de nœud, attaché aux instances qui portent le cluster. Ce dernier n'apparaît dans aucun constat.

Ce que cette correspondance apporte reste réel : une famille définie après la rédaction de ce rapport recouvre son deuxième risque majeur, et deux de ses trois vecteurs y sont déjà mesurés, dont un en point fort. Elle en désigne aussi un troisième qui manque.

La famille « Supply Chain Vulnerabilities » a purement disparu du classement 2025. Aucune famille 2025 ne reprend sa question. Elle portait en 2022 un angle mort réel de cet audit, exposé en §3.11.4.

3.11.2 Les 39 constats rangés, et les 9 qui ne le sont pas

Les constats sont classés par leur famille principale. Un constat qui en touche plusieurs est signalé par une note, il n'est jamais compté deux fois.

Famille (2025) La question qu'elle pose Constats N
K01 Insecure Workload Configurations mes conteneurs tournent-ils avec plus de droits et de ressources que nécessaire ? C-S, C-T, EK-8 🟢 3
K02 Overly Permissive Authorization qui peut faire quoi dans le cluster, et est-ce justifié ? C-Q, C-U 🔴, C-V, C-W, EK-10, EK-13, EK-14 7
K03 Secrets Management Failures mes secrets sont-ils protégés au repos et tournés ? C-G, C-N, C-P, EK-2, AW-10 5
K04 Lack Of Cluster Level Policy Enforcement une politique s'applique-t-elle partout, ou seulement là où on y a pensé ? C-X, EK-3, EK-12 3
K05 Missing Network Segmentation Controls un pod compromis peut-il parler à tous les autres ? C-R, C-Y, EK-6 3
K06 Overly Exposed Kubernetes Components qu'est-ce qui est joignable depuis l'extérieur ? EK-9 1
K07 Misconfigured And Vulnerable Cluster Components mes composants sont-ils bien réglés, et à jour ? C-B 🟢, EK-17 2
K08 Cluster To Cloud Lateral Movement 🆕 une compromission dans le cluster atteint-elle le compte cloud ? AW-2 🔴, AW-3, AW-11, EK-4, EK-16 5
K09 Broken Authentication Mechanisms comment prouve-t-on son identité, et est-ce assez fort ? AW-4, AW-7, AW-8 🟢, AW-9 🔴 4
K10 Inadequate Logging And Monitoring si ça arrivait, le saurais-je, et pourrais-je le reconstituer ? C-A, C-O, EK-1 🟢, EK-5, AW-1, AW-6 🟢 6
Total classé 39
Hors grille (§3.11.4) 9
Total 48

Constats à double famille, comptés une seule fois dans leur famille principale :

Constat Principale Touche aussi Pourquoi
C-Q K02 K01 l'agent lit tous les secrets et tourne en root
EK-3 K04 K05 ni admission ni politique réseau sur les mêmes namespaces
EK-12 K04 K02 les 16 chemins n'existent que parce que l'admission ne borne pas
EK-4, AW-3 K08 K09 une clé statique est un défaut d'authentification et un vecteur de pivot
AW-9 K09 K08 la clé de l'administrateur ouvre le compte, donc tout ce qui suit

3.11.3 Ce que les cases pleines disent

Aucune des dix familles n'est vide. C'est le résultat principal de cette relecture, et il est modeste : la grille ne révèle aucun angle mort de famille. Les trois autres référentiels avaient déjà ramené au moins un constat dans chacune des dix.

⚠️ Ce résultat ne prouve pas grand-chose, et il faut le dire. Répartir 48 constats sur dix familles larges, à la main et par l'auteur du classement, remplit les dix cases dans la plupart des cas de figure. L'absence de case vide est le résultat attendu, pas un exploit. Ce que cette relecture apporte n'est donc pas ici, mais en §3.11.4 : ce qu'elle ne voit pas.

La famille la plus chargée est K02 avec 7 constats, sur l'autorisation. Le verdict de la synthèse (§0) ne l'annonçait pas : il porte sur le blocage, la détection et la trace, pas sur l'étendue des droits. K10 (6) recolle en revanche directement avec deux de ses trois clauses, la détection et la trace.

⚠️ Ce n'est pas une confirmation indépendante. La grille a été appliquée par l'auteur du verdict, après le verdict, sur ses propres constats. Quand elle retombe sur K10, elle reformule, elle ne corrobore pas. Une grille appliquée par l'auteur ne peut pas servir de témoin à charge contre lui-même, et la présenter ainsi serait exactement le défaut que ce rapport dénonce ailleurs.

🔴 K08 porte le risque majeur n°2 du rapport (AW-2) et quatre constats élevés, cinq en tout. AW-9, risque n°1, la touche sans y être classé (voir la table des doubles familles ci-dessus). Une famille créée en 2025 pour les clusters managés capte une part importante du volet cloud, ce qui est cohérent avec §5.7 : le managé déplace le risque, il ne le supprime pas.

🟢 K06 n'a qu'une entrée, et c'est un point fort déguisé. Le homelab n'y apparaît pas du tout, non par défaut de mesure mais parce que §5.4bis démontre qu'il n'expose aucune console sur Internet (ce qui est plus étroit que « son réseau n'est pas exposé », et laisse de côté le tunnel sortant non mesuré de §1.3.6). K06 portant sur les composants Kubernetes, la preuve porte. Seul EK-9 remplit la famille, sur le cluster délibérément exposé. Une case presque vide n'est pas toujours un trou.

3.11.4 Ce que la grille ne voit pas

Neuf constats sur 48 ne tombent dans aucune famille. Ils ne sont pas moins vrais, ils sont hors du périmètre d'une grille qui s'arrête au cluster, et qui ne traite que la confidentialité et l'intégrité.

Constat Pourquoi la grille ne le voit pas
C-M 🔴 aucune famille ne couvre la disponibilité : ni sauvegarde d'etcd, ni plan de contrôle redondant, donc pas de reprise possible
C-F même raison : cohérence d'état après un arrêt brutal, deux pods fantômes
C-D note de méthode (§1.5.4), pas un défaut : deux chiffres à ne pas confondre
C-E note de méthode (§1.3.7) : la portée de la vérification des secrets, pas son résultat
C-L exposition d'un dépôt de travail, hors du cluster et hors du compte cloud
EK-7 sauvegarde d'une base managée : ressource cloud, aucune famille Kubernetes
EK-11 un écrit du dépôt qui contredit le dépôt. Aucune grille technique ne mesure ça
EK-15 qui peut faire entrer un manifeste dans la branche synchronisée. Voir l'arbitrage ci-dessous
AW-5 durcissement d'un bucket de state : compte cloud pur, hors cluster

🔴 Le cas EK-15 est le plus instructif, et il joue contre la grille la plus récente. Il relevait sans ambiguïté de K02 Supply Chain Vulnerabilities en 2022. Cette famille n'existe plus en 2025, donc une grille plus récente le classe moins bien qu'une grille plus ancienne.

L'objection, et pourquoi elle est écartée. On peut soutenir qu'EK-15 relève du K02 de 2025, « Overly Permissive Authorization Configurations » : après tout, c'est bien une question de droits trop larges. L'argument ne tient pas, parce que ces droits ne sont pas dans le cluster. K02 traite de l'autorisation Kubernetes, le RBAC, les rôles, les liaisons, ce que l'API du cluster accorde à qui. EK-15 mesure un droit d'écriture sur une branche Git, accordé par une forge, à des identités que le cluster ne connaît pas. Le ranger en K02 reviendrait à étendre la famille à toute autorisation, où qu'elle vive, ce qui la viderait de son sens. C'est précisément l'espace que la famille « Supply Chain » occupait en 2022, et que 2025 laisse vacant.

Le déclassement d'EK-15 n'est que la partie visible. Le vrai effet de la disparition de cette famille est ailleurs : aucun des 48 constats ne porte sur la chaîne d'approvisionnement des images. C'est un trou de l'audit, et il faut le dire précisément, car le projet, lui, n'est pas vide sur ce terrain :

Contrôle État réel, hors périmètre de cet audit
SBOM ✅ présent, CycloneDX sur les deux images, rattaché au rapport de la forge. Inventaire, pas un point de blocage
Scan de vulnérabilité de l'image bloquant, CRITICAL et HIGH, avec fichier VEX et liste d'exceptions
Signature et attestation de provenance ❌ absentes
Vérification à l'admission de ce qui est déployé ❌ absente

⚠️ Le constat n'est donc pas « le projet ne fait rien », c'est « l'audit n'a rien regardé ». Ces quatre lignes ne sont ni des constats ni des actions : elles sont hors du périmètre gelé (§1.2) et n'ont pas été éprouvées par la méthode de ce rapport. Elles sont citées ici pour que l'absence de constat ne se lise pas comme une absence de contrôle. Leur examen relève d'un prochain audit.

⚠️ La grille 2022 aurait forcé cette question, la grille 2025 ne la pose plus. Un référentiel plus récent n'est pas automatiquement plus exigeant : il est réordonné selon ce que son éditeur juge le plus fréquent aujourd'hui, et ce qui sort de son classement ne cesse pas d'exister. C'est la limite de fond de tout exercice de relecture par grille, et la raison pour laquelle ce rapport en emploie quatre et non une.

3.11.5 Ce que cette relecture change au rapport

Effet Détail
§1.4 tient sans réserve les quatre référentiels annoncés produisent désormais tous une trace vérifiable dans le rapport. Avant cette section, OWASP était annoncé et n'apparaissait nulle part
AW-2 gagne une caution extérieure il incarne une famille K08 créée après sa rédaction
🟠 Un trou de couverture nommé la chaîne d'approvisionnement des images : l'audit n'a produit aucun constat dessus, alors que le projet y a des contrôles réels et non éprouvés (§3.11.4). La grille 2025 ne réclame plus cette question
Aucun constat nouveau la grille n'a produit aucune découverte. C'est le résultat attendu d'une relecture, pas un échec

Aucune action de remédiation n'est ajoutée par cette section. La chaîne d'approvisionnement relève du périmètre d'un prochain audit, pas d'une correction sur l'existant : rien n'y a été mesuré, donc rien n'y est démontré, dans un sens comme dans l'autre.


4. Purple team

4.1 Méthode

Purple team, pas red team. Une red team simule un attaquant sans prévenir la défense. Impossible à conduire seul, et sans intérêt ici. Le purple team joue l'attaque et vérifie la réponse défensive. C'est la méthode adoptée.

Pour chaque scénario, trois questions, toujours les mêmes :

  1. Est-ce que ça a été bloqué ?
  2. Est-ce que ça a été détecté ?
  3. Est-ce que ça a laissé une trace consultable après coup ?

Chaque scénario est tiré d'une technique MITRE ATT&CK for Containers. Ce n'est pas une liste inventée. Une tactique est l'objectif de l'attaquant (persistance, évasion, exfiltration), une technique est le moyen concret, identifié T####.

Deux règles de conduite, appliquées sans exception :

  • Toute ressource témoin créée a été supprimée, et la suppression vérifiée par un NotFound explicite.
  • Pour tout résultat « bloqué », une contre-épreuve. Un échec de connexion n'est pas une preuve de protection. Il faut prouver par une cible qui, elle, répond.

4.2 Couverture MITRE

Scénario Technique Tactique Testé sur
PT-1 jeton de ServiceAccount T1552.007 Container API Credential Access homelab ✅ · EKS ✅
PT-2 secrets au repos T1552.001 Credentials In Files Credential Access homelab ✅ · EKS ✅
PT-3 cloisonnement réseau interne T1046 Network Service Discovery Discovery homelab ✅ · EKS ✅
PT-4 authentification applicative T1078 Valid Accounts Initial Access homelab ✅ · EKS ✅
PT-5 évasion vers l'hôte T1611 Escape to Host Privilege Escalation homelab ✅ · EKS ✅
PT-6 identifiants d'instance (IMDS) T1552.005 Cloud Instance Metadata API Credential Access sans objet sur homelab · EKS ✅
PT-7 exécution dans un conteneur T1609 Container Administration Command Execution homelab ✅ · EKS ✅
PT-8 persistance par charge planifiée T1053.007 Container Orchestration Job Persistence homelab ✅ · EKS ✅
PT-9 détournement de ressources T1496 Resource Hijacking Impact homelab ✅ · EKS ✅
PT-10 sortie vers Internet T1041 Exfiltration Over C2 Channel Exfiltration homelab ✅ · EKS ✅

Volontairement écartés : T1610 Deploy Container et T1525 Implant Internal Image, déjà couverts par la chaîne d'intégration continue. T1612 Build Image on Host ne s'applique pas, aucun démon Docker n'est joignable depuis un pod.

4.3 Résultats sur le cluster homelab

Campagne complète. Neuf scénarios joués, un sans objet.

# Scénario Bloqué ? Détecté ? Trace ?
PT-1 jeton de ServiceAccount oui, deux fois, par le RBAC ❌ non ❌ non
PT-2 secrets au repos non, lus en clair dans etcd ❌ non ❌ non
PT-3 cloisonnement interne 🟡 partiel, là où une politique existe ❌ non ❌ non
PT-4 authentification applicative oui, partout ❌ non ❌ non
PT-5 évasion vers l'hôte 🟡 dépend du namespace 🟡 un Warning seulement ❌ non
PT-6 IMDS — sans objet hors cloud
PT-7 exécution dans un conteneur ❌ non (droit légitime) non non
PT-8 persistance par CronJob ❌ non non 🟡 l'objet existe, rien ne le signale
PT-9 détournement de ressources 🟡 par accident, la capacité ❌ non ❌ non
PT-10 sortie vers Internet non, aucun filtrage ❌ non ❌ non

🔴 Aucun des neuf scénarios ne laisse de trace journalisée consultable après coup. C'est le résultat le plus important de cette campagne, et sa cause racine est C-O.

Une seule nuance, et elle est dans la table ci-dessus : PT-8 laisse un objet résiduel, le CronJob lui-même, qui reste visible tant que personne ne le supprime. Ce n'est pas une trace au sens de cette campagne : rien ne le signale, et il disparaît avec l'objet. Un attaquant qui nettoie derrière lui ne laisse donc rien du tout. La distinction entre « un objet subsiste » et « un événement a été enregistré » est exactement ce que mesure la troisième question.

PT-1 — Ce que peut un compte applicatif compromis

Chemin joué : Gateway → todos-demo → jeton de ServiceAccount → API Kubernetes.

Étape 1, le jeton est bien là. Les trois pods de todos-demo utilisent le ServiceAccount default, et automountServiceAccountToken n'est pas positionné, donc la valeur par défaut s'applique et le jeton est monté. Vérifié dans le pod : ca.crt, namespace et token présents.

⚠️ Point de lecture : une colonne AUTOMOUNT vide ne veut pas dire false. Elle veut dire non renseigné, donc le défaut s'applique, donc le jeton est monté.

Étape 2, le jeton ne donne rien. Interrogation de l'API depuis l'intérieur du pod :

Cible Réponse
/api/v1/namespaces/todos-demo/secrets HTTP 403 Forbidden
/api/v1/nodes HTTP 403 Forbidden

403 et non 401, et la distinction est tout le constat : le jeton authentifie correctement, l'API sait qui appelle, c'est l'autorisation qui refuse. Le RBAC fait son travail.

Bonus défensif mesuré : curl est absent de l'image. Une image minimale prive l'attaquant de son outillage. Le test a dû passer par Python, présent seulement parce que l'application est en FastAPI.

Remédiation : automountServiceAccountToken: false sur les charges qui n'appellent pas l'API. Cela ne corrige pas un droit, cela supprime un jeton inutile. La surface diminue, pas le privilège. C'est déjà ce que fait le cluster EKS (EK-8).

PT-7 — L'exécution dans un conteneur, et l'hypothèse centrale du cadrage

Le test le plus important de la campagne, parce qu'il mesure directement l'hypothèse posée au cadrage : le blocage tiendra, la détection sera le trou.

Un kubectl exec est produit volontairement avec un marqueur horodaté, puis recherché dans Loki. Le critère discrimine par un cas témoin :

Cas Attendu Mesuré
A, le marqueur de l'exec 0 0 ligne
B, les logs normaux du même pod, témoin > 0 5 lignes, à +10 secondes

Le zéro du cas A n'est pas une panne de Loki. Le cas B prouve que la collecte fonctionne en temps quasi réel. Sans ce témoin, le résultat n'aurait rien valu.

🔴 Un exec dans un conteneur du homelab ne laisse aucune trace, nulle part. Ni journal d'audit de l'API, qui n'existe pas, ni logs de conteneur, la sortie d'un exec n'étant pas celle du conteneur.

L'hypothèse du cadrage est mesurée, pas supposée.

PT-8 — ArgoCD n'est pas un contrôle de sécurité

todos-demo est géré par ArgoCD en mode automated, mais avec prune: false. Un CronJob intrus est créé, puis un rafraîchissement forcé est demandé.

Après rafraîchissement Résultat
Statut de l'application Synced / Healthy
Le CronJob intrus toujours présent

🔴 Le pire des trois cas. Avec prune: false, ArgoCD ignore les ressources absentes de Git : il ne les voit pas comme une dérive. Ni blocage, ni détection. Une persistance installée par un attaquant ne serait jamais signalée.

Distinction importante, et souvent confondue : pour une ressource ajoutée, c'est prune qui compte, pas selfHeal. selfHeal ne fait que rétablir les ressources suivies qu'on modifie.

Vérifié sur EKS le 2026-08-23 (PT-8, détail en §4.4), et le résultat est plus fort que prévu. gitlab-ci-infra porte prune: true ET selfHeal: true — le réglage inverse du homelab — et l'application reste Synced malgré le CronJob intrus. Ce n'est donc pas le réglage prune qui est en cause : ArgoCD ne surveille que ce qu'il connaît, quel que soit ce réglage. Le motif se généralise, et il est plus général que ce que la question posée ici anticipait.

PT-10 — La sortie vers Internet n'est filtrée par rien

Depuis todos-backend, quatre destinations testées.

Cible Résultat
example.com:443, :80, 1.1.1.1:53 SORTIE OK
example.com:22 TimeoutError
contre-épreuve : github.com:22, qui écoute vraiment sur ce port SORTIE OK

⚠️ Le TimeoutError initial n'était pas un blocage. example.com n'écoute simplement pas sur le port 22. Sans la contre-épreuve, ce résultat aurait été écrit comme une protection.

github.com:22 sort, donc aucun filtrage en sortie. Confirmé par l'absence de toute NetworkPolicy dans todos-demo.

Les trois pièges de lecture rencontrés

Chacun aurait produit un constat faux. Ils sont consignés parce que c'est la méthode qui a de la valeur pour un lecteur, autant que le résultat.

# Piège Ce qu'il aurait fait croire
1 un label Loki interrogé sans plage de temps répond sur la dernière heure que kube-bench ne remonte plus rien, alors qu'il n'a simplement aucun pod actif entre deux exécutions hebdomadaires
2 un TimeoutError sur un port fermé qu'un filtrage réseau bloque, alors que rien ne filtre
3 un Warning PSA qui crée quand même le pod qu'un pod privilégié a été refusé, alors qu'il existe

4.4 Résultats sur le cluster EKS

Campagne complète, jouée le 2026-08-23 entre 12h07 et 13h55 UTC. Dix scénarios définis, dont PT-2 sans vecteur sur EKS : neuf mesurables. Contrairement au homelab, PT-6 (IMDS) s'applique : le cluster tourne sur des instances EC2.

# Scénario Bloqué ? Détecté ? Trace ?
PT-1 jeton de ServiceAccount oui, jeton absent + RBAC vide ❌ non survit à la destruction (CloudWatch)
PT-2 secrets au repos vecteur inexistant, encryptionConfig nul (EK-2)
PT-3 cloisonnement interne oui, default-deny partout ❌ non ✅ survit
PT-4 authentification applicative oui, depuis Internet, sans identifiant ❌ non ✅ survit
PT-5 évasion vers l'hôte 🟡 dépend du namespace, comme au homelab 🟡 un Warning seulement ✅ survit
PT-6 IMDS oui, IMDSv2 + hop_limit=2 (EK-8) — applicatif, pas un contrôle de détection ✅ survit
PT-7 exécution dans un conteneur ❌ non (droit légitime) 🟡 l'appel d'API, pas l'exécution survit, écart le plus net du comparatif
PT-8 persistance par CronJob 🟡 dépend du namespace (PSA, pas GitOps) ❌ non ✅ survit (l'objet existe, personne ne le signale)
PT-9 détournement de ressources oui, sur les deux namespaces testés ❌ non ✅ survit
PT-10 sortie vers Internet 🟠 partiel : angle mort confirmé sur le 443 sans clause to ❌ non ✅ survit

🟢 La colonne « trace » se remplit presque entièrement, écart le plus net du comparatif avec le homelab. Le journal d'audit du plan de contrôle managé (EK-1) capture l'appel d'API de chaque scénario, et la trace survit à la destruction du cluster (AW-6, prouvé sur PT-7). Ce que le journal ne voit toujours pas, c'est le contenu de l'exécution — colonne « détecté », toujours vide ou partielle.

PT-1 — Bloqué, et meilleur que le homelab sur deux points

a. Jeton absent, pas seulement inutile. automountServiceAccountToken: false sur le ServiceAccount fastapi (EK-8, confirmé en vol) : l'appel échoue en No such file or directory, avant même d'atteindre l'API.

b. Un triplet de jetons, joué pour comparer les deux clusters sur le même protocole :

Cas Homelab (17/08) EKS (23/08)
jeton valide 403 Forbidden (sans objet, jeton non monté)
jeton invalide (BIDON) 401 401
aucun en-tête (SANS) 403 401

🟢 L'écart sur SANS est le point à retenir. Au homelab, une requête sans jeton est authentifiée comme system:anonymous, une identité réelle que le RBAC refuse ensuite (403). Sur EKS, l'authentification anonyme est désactivée : la requête n'est authentifiée à aucun titre (401). Deuxième asymétrie franche en faveur du managé, après le journal d'audit.

c. Le ServiceAccount applicatif, testé en questions fermées, n'a aucun droit : create pods, get secrets dans son propre namespace, get secrets dans fastapi-prod, et list nodes répondent tous no. Le cloisonnement inter-environnement tient au niveau RBAC.

PT-2 — Le vecteur du homelab n'existe pas sur EKS, et c'est le constat

Le test homelab exigeait un exec dans le pod etcd du plan de contrôle. Sur EKS, ces machines appartiennent à AWS (§1.3.3) : le test est structurellement impossible, pas raté.

Homelab (17/08) EKS (23/08)
secret lu en clair dans etcd, témoin supprimé et vérifié NotFound vecteur inexistant, seule mesure possible : encryptionConfig

Seul reste mesurable encryptionConfig = null, déjà tranché dans EK-2. Le managé retire un vecteur du périmètre auditable, il ne le corrige pas — c'est la même nuance que 1.3.3.

PT-3 — Le cloisonnement egress fonctionne, écart net avec le homelab

Trois cibles internes interrogées depuis fastapi-dev, toutes en TimeoutError — la bonne signature d'un filtrage réel, un paquet jeté en silence plutôt qu'un ConnectionRefused :

BLOQUE  kube-prometheus-stack-grafana.monitoring.svc  80
BLOQUE  argocd-server.argocd.svc                      80
BLOQUE  external-secrets-webhook.external-secrets.svc 443
ATTEIGNABLE  kubernetes.default.svc                    443

Contre-épreuve obligatoire, jouée. 15 NetworkPolicy inventoriées, toutes restrictives : aucune ne porte de règle vide (ingress: [{}] ou egress: [{}]). Le piège du homelab — une politique qui apparaît dans kubectl get mais n'autorise en réalité que du vide — n'existe pas ici. Les TimeoutError mesurés étaient donc de vrais blocages.

EK-3 confirmé sur ce test précisément : le default-deny-all egress couvre les 7 namespaces applicatifs, le homelab n'en couvrait aucun (C-R, 13 namespaces sur 14 sans politique). Écart net et mesuré, en faveur d'EKS.

PT-4 — L'authentification tient, dans des conditions plus dures qu'au homelab

Test le plus rentable de la campagne (§3.4, EK-9) : joué depuis Internet, sans aucun identifiant.

grafana/api/org                 -> 401  {"message":"Unauthorized"}
argocd/api/v1/applications      -> 401  {"error":"no session information"}
argocd/api/v1/clusters          -> 401  {"error":"no session information"}
argocd/api/v1/session/userinfo  -> 200  {}

⚠️ Le 200 sur userinfo n'est pas une faille, même piège de lecture qu'au homelab : c'est un endpoint public par conception, répondant « personne » ({}). À lire le corps, jamais seulement le code.

🟢 Les deux consoles publiques (EK-9) tiennent, et le test est asymétrique en défaveur d'EKS. Côté homelab, PT-4 a été joué depuis l'intérieur du cluster, après chute du cloisonnement. Côté EKS, il est joué depuis l'extérieur, sur Internet, sans jeton. EKS tient dans des conditions strictement plus défavorables. EK-9 reste une exposition (aucun filtre par adresse source), mais l'authentification applicative qui la protège a été vérifiée dans les pires conditions, pas supposée.

PT-5 — L'admission prouvée par l'effet, la chaîne vers root est fermée d'un côté seulement

Pod privileged: true montant hostPath: /, joué dans deux namespaces :

Cas Namespace PSA Verdict
1 fastapi-dev restricted Forbidden, 6 violations énumérées d'un coup
2 monitoring aucun label pod/audit-pt5-temoin created

🔴 Un pod réellement privilégié, montant / du nœud, admis sans objection dans un namespace de plateforme. C'est la preuve par l'effet qui manquait au maillon 2 de la chaîne AW-11 et au constat EK-3 / EK-12 : ce n'est plus une lecture déclarative de labels, c'est un pod qui naît.

accès namespace plateforme (EK-12 : ArgoCD + cert-manager, 16 chemins)
  -> créer un pod privilégié (PT-5 : admis)
  -> monter / du nœud -> root sur le nœud

Même durcissement inversé par rapport au risque qu'au homelab : les 7 namespaces applicatifs refusent six choses d'un coup, les 8 (ou 9, voir §3.4) de plateforme n'opposent rien, alors que c'est la plateforme qui porte ArgoCD cluster-admin (C-U) et la chaîne IRSA (AW-11).

PT-6 — IMDSv2 tient, mais il a fallu une contre-épreuve pour le prouver

Premier résultat, non concluant : IMDSv1 et IMDSv2 bloqués depuis fastapi-dev, toutes deux en URLError.

⚠️ Piège identifié avant d'écrire un constat. L'egress de fastapi-dev est en default-deny et n'ouvre pas le port 80 (PT-3 vient de le prouver). Le blocage pouvait venir de la NetworkPolicy, pas de l'IMDS — deux causes possibles, une seule mesure, motif critere-qui-ne-discrimine-pas.

Contre-épreuve, obligatoire avant conclusion : rejouée depuis un pod de monitoring, sans aucune politique réseau. Réponse : HTTP/1.1 401 Unauthorized.

🟢 Un 401 est un refus applicatif de l'IMDS, pas un timeout réseau. La contre-épreuve tranche sans ambiguïté : le blocage vient bien d'IMDSv2 obligatoire (http_tokens=required) et de hop_limit=2 (terraform/modules/eks/main.tf). EK-8 confirmé en vol, point fort prouvé proprement, pas seulement lu dans le dépôt.

🔵 Nuance à écrire avec PT-2 : le cloud offre les bons contrôles, encore faut-il les activer. IMDSv2 l'est (prouvé ici), le chiffrement KMS ne l'est pas (EK-2). « Le cloud est plus sûr » est faux ; « le cloud fournit les contrôles, l'exploitant les active ou non » est juste.

PT-7 — L'hypothèse centrale du cadrage, nuancée par la mesure

Le test le plus important de la campagne EKS, symétrique de PT-7 côté homelab.

Un exec marqué (audit-pt7-1787488120) est joué à 12:28:41 UTC dans fastapi-dev.

Deux pièges de mesure payés avant d'obtenir un résultat exploitable, tous deux critere-qui-ne-discrimine-pas : 1. le filtre "pods/exec" sur le journal CloudWatch rend [] : la chaîne littérale n'existe pas, l'URL porte le nom du pod au milieu (.../pods/fastapi-xxx/exec). Le [] ne prouvait rien, ni un flux mort ni une absence de trace. 2. le filtre "subresource":"exec" est refusé par CloudWatch (les : cassent le motif). Retenu : filtrer sur "exec", trier subresource=='exec' côté poste.

D'abord prouver que le flux reçoit (3 timestamps distincts remontés), avant de chercher l'exec précis :

user/youss_admin | fastapi-dev fastapi-f56495756-l7hbs | 12:28:41.23

L'exec est journalisé, avec l'auteur par son ARN IAM complet. EK-1 confirmé en vol. L'investigation est possible : qui, quand, quel pod.

🔴 Mais ce qui est journalisé est l'appel d'API, pas le contenu de la session. Le journal d'audit voit la porte exec s'ouvrir. Les commandes tapées dans le conteneur n'apparaissent nulle part. Le vrai trou n'est pas « aucune trace », c'est l'absence de détection à l'exécution (comportementale, type Falco/Tetragon). L'hypothèse du cadrage se nuance : sur EKS, l'audit du plan de contrôle voit l'exec, mais rien n'observe ce qui se passe ensuite.

🟢 Écart le plus net du comparatif. La même commande, jouée au homelab, ne rend rien — ni journal d'audit (C-O), ni CloudTrail. Sur EKS elle rend auteur, heure et cible.

Fait le 23/08 à 14h38 UTC, cluster détruit depuis 16h36 CEST : la trace survit. L'exec de 12:28:41 est retrouvé dans CloudWatch, avec son auteur, alors que l'api server qui l'a produit n'existe plus (AW-6). Bonus : tous les exec de la journée ont survécu, pas seulement celui-ci.

Question ouverte, tranchée à froid : ces exec étaient-ils retrouvables dans Grafana, plutôt que dans la console AWS ? Posée le 23/08, non vérifiable cluster monté (question posée après le teardown). Vérifiée sur le dépôt au commit de gel : Grafana n'a que deux datasources, datasource-loki.yaml et datasource-tempo.yaml (plus Prometheus par le chart). Aucune datasource CloudWatch. Réponse : non, il n'était pas possible de retrouver l'exec dans Grafana.

🔵 Ce que ça prouve, et ça renforce le constat. La stack d'observabilité en cluster (Loki/Grafana/Tempo) a un angle mort sur l'audit du plan de contrôle. Elle voit les logs applicatifs et les traces, pas les appels d'API sensibles comme exec, qui ne vivent que dans CloudWatch, hors cluster. Recommandation, plan de remédiation : une datasource CloudWatch dans Grafana fermerait cet angle mort, avec deux réserves : ça change la vue, pas la source (vient en plus de la rétention à poser, pas à sa place) ; ça demande un rôle IRSA dédié et restreint (logs:GetLogEvents, logs:StartQuery), à ne surtout pas fusionner avec un rôle existant vu le défaut de cloisonnement d'AW-11.

Les trois questions de la méthode, closes sur ce scénario :

Question EKS Homelab
1. Bloqué ? ❌ non (droit légitime) ❌ non (droit légitime)
2. Détecté ? 🟡 l'appel d'API, pas l'exécution ❌ rien (C-O)
3. Trace après coup ? survit à la destruction du cluster rien : ni audit API (C-O, inexistant), ni logs de conteneur (l'exec n'y apparaît pas)

PT-8 — Persistance : le PSA arrête ce qu'ArgoCD ne voit pas

CronJob */5 posé dans fastapi-dev (PSA restricted) à 13:17:25 UTC.

Objet État Cause
CronJob ✅ créé, ACTIVE 1 le PSA avertit les contrôleurs, ne les bloque pas
Jobs créés toutes les 5 min idem, un Job est un contrôleur
Pods FailedCreate, aucun ne naît l'enforce du PSA s'applique aux Pods, refusés (4 violations)

🔴 La distillation vaut d'être citée telle quelle : « le Job créé dont aucun pod ne sortira ». kubectl apply rend created, les Jobs tournent réellement toutes les 5 minutes, et aucun pod n'existe jamais. L'enforce du PSA n'évalue que les Pods, pas les gabarits des ressources de charge.

🔴 ArgoCD n'est pas un contrôle de sécurité ici non plus, et c'est le point à retenir : la cause diffère du homelab. Application fastapi-dev restée Synced alors qu'un CronJob étranger vivait dans son namespace, malgré prune: true ET selfHeal: true — le réglage inverse du homelab (prune: false). ArgoCD ne gère que ce qui porte son étiquette de suivi ; un objet hors dépôt lui reste invisible, quel que soit le réglage de prune.

Ce que la comparaison des deux clusters établit, et qui dépasse ce constat : le réglage prune change ce qui se passe pour une ressource suivie. Il ne change rien pour une ressource jamais suivie, qui est le cas d'un objet injecté par un attaquant. Le motif est donc plus général que « mal configurer prune » : GitOps ne voit que ce qu'il connaît.

⚠️ Nuance décisive, elle referme la chaîne : ce blocage tient parce que fastapi-dev est restricted. Dans un namespace de plateforme sans PSA (PT-5), le même CronJob produirait des pods. La persistance serait réelle côté plateforme. La chaîne EK-12 → PT-5 → PT-8 se referme précisément là où le rapport recommande d'agir en premier (plan de remédiation, action 10).

PT-9 — Les quotas bornent à l'admission, et EK-3 ne se prolonge pas sur cette couche

Pod demandant cpu: 8, memory: 16Gi, joué dans deux namespaces.

fastapi-dev porte un ResourceQuota (limits.memory 1Gi, pods 4, requests.memory 512Mi) et un LimitRange (limite mémoire par défaut 256Mi).

Cas Namespace Verdict
1 fastapi-dev refusé à l'admission : requête 16Gi > limite 256Mi
2 monitoring refusé aussi

🟢 Cas 1 : la charge n'existe jamais. Un contrôle de stabilité (issues #136, #149, incident INC-061 du dépôt) agit ici comme contrôle de sécurité — T1496, minage de ressources, borné avant même de démarrer.

🔴 Le cas 2 corrige une hypothèse posée plus tôt dans ce rapport. monitoring était supposé sans quota ni LimitRange, comme les autres namespaces de plateforme. Vérifié en vol : limitrange/default-limits y existe, créé au bootstrap (2026-08-23T10:20:23Z).

EK-3 ne se prolonge donc pas sur la couche quotas :

Couche Applicatif Plateforme (monitoring) EK-3 tient ?
Réseau (NetworkPolicy) fermé ouvert
Admission (PSA) restricted aucun (PT-5)
Quotas (LimitRange) présent présent aussi

Écrit tel quel plutôt que passé sous silence : un lecteur qui teste monitoring trouverait le LimitRange, et un rapport qui l'aurait caché perdrait toute sa crédibilité sur le reste.

PT-10 — L'angle mort confirmé, exactement comme prédit le 18/08

SORTIE OK   example.com 443
BLOQUE      example.com 80  TimeoutError
BLOQUE      1.1.1.1     53  TimeoutError
BLOQUE      example.com 22  TimeoutError

🟠 Résultat exactement conforme à la prédiction écrite cinq jours plus tôt. La règle allow-fastapi ouvre l'egress sur le port 443 sans clause to (k8s/base/networkpolicy-fastapi.yaml). Elle existe pour ECR et Secrets Manager ; elle autorise en fait n'importe quelle destination en HTTPS. Un pod compromis peut exfiltrer vers n'importe où tant que le port et le protocole collent. Le cloisonnement mesuré en PT-3 ne couvre pas cette sortie précise.

Détail qui vaut d'être noté : 1.1.1.1:53 est bloqué alors que la résolution DNS fonctionne (example.com a bien résolu). Le DNS passe donc par CoreDNS du cluster, pas par un résolveur externe — bon point, à ne pas confondre avec un blocage général du port 53.

Recommandation, peu coûteuse et déjà écrite d'avance : ajouter une clause to sur la règle 443, une règle qui existe déjà, il suffit de la resserrer vers les points de terminaison ECR et Secrets Manager. Effort : faible.

Q12 — ArgoCD sert en clair sur les deux clusters

kubectl -n argocd get cm argocd-cmd-params-cm
# server.insecure: "true"

Même parti pris qu'au homelab, sur deux infrastructures opposées. Le TLS s'arrête à la passerelle, un jeton transitant en interne serait lisible par qui écoute le réseau du cluster — même motif que C-Y (§3.4), mais confirmé sur EKS aussi, comme C-U et C-V : c'est un défaut qui se généralise, pas une particularité d'une installation.

Pièges rencontrés côté EKS, et pourquoi ils sont consignés

# Piège Ce qu'il aurait fait croire
1 filtre littéral "pods/exec" sur CloudWatch, rend [] qu'aucun exec n'est journalisé, alors que le nom du pod coupe la chaîne en deux
2 TimeoutError sur l'IMDS sans contre-épreuve qu'IMDSv2 bloque, alors que la NetworkPolicy bloquait déjà tout, IMDS compris
3 une question posée après le teardown (« vu dans Grafana ? ») qu'elle resterait sans réponse, alors qu'elle se tranche à froid sur le dépôt

Le motif commun aux trois : une réponse vide ou un blocage mesuré une seule fois, sans seconde mesure qui isole la cause. Identique aux pièges du homelab (§4.3), sur un cluster entièrement différent — la méthode généralise mieux que les résultats.

4.5 Volet compte AWS

Ce volet est clos, et il l'est gratuitement, sans montée de cluster.

Le rapport mitre_attack_aws de prowler couvre le compte sans travail manuel de scénario. Mais il fallait mesurer son périmètre réel avant de s'appuyer dessus.

Répartition des 589 contrôles par service :

Service Contrôles Part
IAM 463 79 %
CloudWatch 18 3 %
Config 17 3 %
S3 13 2 %
GuardDuty 12 2 %
Secrets Manager, Security Hub, Organizations, Inspector2, KMS 26 4 %
non automatisables 40 7 %

🔴 EKS : 0 contrôle. EC2 : 0. VPC : 0. ELB : 0.

Vérifié par recherche directe : le seul contrôle dont l'identifiant contient vpc est cloudwatch_changes_to_vpcs_alarm_configured, qui porte sur une alarme, pas sur un VPC.

Deux conséquences, l'une pour le rapport, l'autre pour le budget :

  1. Le mapping MITRE de prowler décrit le compte, pas l'infrastructure. Ces contrôles rendront exactement le même verdict cluster monté ou détruit. Le volet AWS du purple team est donc terminé.
  2. Rejouer prowler cluster monté reste utile, mais pas pour MITRE. Ce qui changera, ce sont les référentiels CIS et Foundational Security Best Practices : groupes de sécurité EKS, rôles des nœuds, chiffrement des volumes. Le livrable du second scan est le diff CIS.

4.6 Le diff CIS, ou pourquoi un taux global ne se compare jamais tel quel

Piège rencontré avant la méthode correcte, et il vaut d'être montré en premier. Comparer les deux scans prowler bruts, tous référentiels confondus, donne :

2026-08-17 2026-08-23
Contrôles PASS + FAIL 225 465
Taux d'échec 48,4 % 33,8 %

🔴 Lu naïvement, ce chiffre dit que le compte s'est amélioré. C'est faux, et dangereusement faux : entre les deux dates, 48 échecs supplémentaires sont apparus (109 → 157), sans qu'aucune configuration existante n'ait changé. Le taux baisse uniquement parce que monter le cluster a presque doublé le nombre de contrôles applicables (225 → 465) : de nouveaux contrôles EC2, EKS, RDS et VPC, la plupart déjà en échec par défaut, diluent le dénominateur.

C'est l'exemple concret de la règle posée en 1.5.1 et 1.5.4. Un taux de conformité sans sa date ni son dénominateur ne veut rien dire. Publier « le compte s'est amélioré de 15 points » à partir de ce chiffre aurait été le constat faux le plus visible de tout ce rapport.

La bonne méthode : contrôle par contrôle, sur l'intersection

Un seul référentiel, comparé sur les contrôles évalués aux deux dates. Référentiel retenu : CIS AWS Foundations Benchmark v5.0 (le plus récent des sept versions que prowler a rendues), pris au niveau de chaque exigence numérotée (1.13, 5.7, etc.), pas au niveau du contrôle technique brut — une exigence peut agréger plusieurs contrôles et plusieurs ressources, prowler la fait échouer dès qu'une seule ressource échoue.

Valeur
Exigences évaluées le 17/08 41
Exigences évaluées le 23/08 53
Intersection (comparables aux deux dates) 41
Nouvelles le 23/08 (réseau, EC2, RDS — inexistants avant la montée) 12
Disparues entre les deux dates 0

Sur les 41 exigences communes, verdict identique aux deux dates pour les 41 : 32 en échec stable, 8 en réussite stable, 1 manuelle. Zéro exigence n'a changé de statut au niveau agrégé.

🔴 Et pourtant, une dégradation réelle s'est produite dans cet intervalle, invisible à ce niveau d'agrégation. L'exigence 1.13 (rotation des clés d'accès IAM) est en échec les deux fois — donc « sans changement » dans ce tableau. Mais le détail par ressource, déjà établi en AW-3, montre que youss_admin est passée de réussite à échec entre les deux scans : un quatrième utilisateur a franchi le seuil des 90 jours. L'exigence contenait déjà un échec le 17/08 (gitlab-ci, gitlab-ci-infra, iamadmin), donc son verdict agrégé ne bouge pas — la dégradation se cache derrière un échec préexistant.

Ce que ça enseigne, au-delà de ce rapport. Un diff au niveau du contrôle protège du piège du dénominateur global, mais pas de celui-ci : une exigence déjà en échec peut se dégrader silencieusement, tant qu'elle ne franchit pas de PASS à FAIL. Seule la lecture au niveau de la ressource individuelle (celle d'AW-3) l'aurait révélé. Aucun niveau d'agrégation n'est suffisant seul ; c'est pour cette raison que ce rapport cite systématiquement la ressource, pas seulement le contrôle.

Les 12 exigences nouvellement évaluables, une fois le réseau et les bases montés

Exigence Sujet Verdict Constat du rapport
1.17 rôles d'instance EC2 utilisés ✅ PASS
2.2.1 chiffrement au repos RDS ✅ PASS
2.2.2 montée mineure automatique RDS ✅ PASS
2.2.3 RDS non exposée publiquement ✅ PASS
2.2.4 RDS Multi-AZ 🔴 FAIL EK-7, complété (arbitrage assumé)
3.7 flux VPC journalisés 🔴 FAIL EK-5, dédié et confirmé
5.1.1 chiffrement EBS par défaut 🔴 FAIL EK-2, addendum (volumes de nœuds, pas etcd)
5.1.2 CIFS non exposé ✅ PASS
5.2 NACL, ports d'administration 🔴 FAIL voir nuance ci-dessous
5.3 groupes de sécurité, ports d'administration ✅ PASS
5.4 idem, en IPv6 ✅ PASS
5.7 IMDSv2 obligatoire ✅ PASS EK-8, confirmé en vol par PT-6

Nuance sur 5.2 / 5.3 : les groupes de sécurité (5.3, 5.4) bloquent déjà tout accès public aux ports d'administration (22, 3389). Les NACL (5.2), une couche de filtrage plus grossière et sans état, en amont des groupes de sécurité, n'imposent pas la même restriction. Le risque réel reste faible : il faudrait que la barrière des groupes de sécurité tombe pour que l'absence de NACL restrictive compte. C'est un manque de défense en profondeur, pas une exposition. Recommandation : répliquer la restriction au niveau NACL, coût nul, cohérence des deux couches. Effort : très faible.

Bilan du diff CIS. Huit des douze exigences nouvellement visibles réussissent déjà. Les quatre échecs (2.2.4, 3.7, 5.1.1, 5.2) rejoignent des constats existants ou une nuance déjà écrite, plutôt que d'en créer de nouveaux — 5.2 étant celui dont la nuance ci-dessus montre que les groupes de sécurité couvrent déjà le risque, et qui porte l'action 14 du plan. Aucun n'était mesurable avant la montée du cluster, ce qui confirme la logique de 1.3.3 : le managé ne corrige pas ce qu'il retire du périmètre auditable au repos, il le rend simplement mesurable une fois monté.


5. Comparatif : EKS managé contre kubeadm auto-hébergé

C'est l'atout de ce rapport. Les deux clusters ont été audités avec la même méthode, les mêmes outils aux mêmes versions, et les mêmes scénarios. Ce qui suit n'est donc pas une opinion sur le managé, c'est une mesure.

⚠️ À lire avant cette section : §1.3.8. Même méthode ne veut pas dire même âge de projet. Le homelab a clos un sprint, EKS en a clos six, et plusieurs chantiers du homelab sont planifiés, pas absents (#80 NetworkPolicy, sauvegarde etcd, HA). Un comparatif à maturité égale serait plus équitable ; le calendrier ne l'a pas permis, et cet audit a lui-même décalé ces sprints.

Règle de comparaison, posée avant tout chiffre : jamais de comparaison de scores bruts, toujours contrôle par contrôle, sur l'intersection de ce qui a été évalué des deux côtés (section 1.5.1).

5.1 Axe 1 — Journalisation du plan de contrôle : le managé la donne en cochant une case

Homelab EKS
Journal d'audit de l'API aucun (C-O) activé (EK-1)
Comment on l'obtient politique d'audit à écrire, fichier à collecter, chaîne à brancher enabled_cluster_log_types, une ligne de Terraform
Où va la trace nulle part CloudWatch
Le journal d'audit survit-il à la destruction du cluster ? sans objet, il n'existe pas (C-O) oui, prouvé le 19/08, contenu compris, et hors du cluster
Ce qui garantit la survie côté EKS une rétention illimitée que personne n'a choisie
Un kubectl exec laisse-t-il une trace ? non (PT-7) oui, appel d'API + auteur, survit à la destruction (PT-7, §4.4)

Ce que ça dit. Sur ce point, le service managé ne fait pas mieux techniquement : il rend simplement accessible en une case à cocher ce qui demande une chaîne complète à monter soi-même. La différence est le coût d'accès au contrôle, pas le contrôle.

Une nuance à ne pas confondre avec ce qui précède. La chaîne d'observabilité du homelab (Prometheus/Loki/Grafana, §2.1.5) survit bien, elle, à une extinction du cluster : les verdicts kube-bench écrits avant l'arrêt restent interrogeables au rallumage, parce que Loki écrit sur un volume persistant de l'hyperviseur. Mais ce n'est pas la même chose qu'un journal d'audit de l'API, et ça n'aide pas sur PT-7 : un kubectl exec n'est capturé par aucun des deux mécanismes, pas parce que le volume disparaît, mais parce que rien ne l'a jamais journalisé (ni l'audit API absent, ni les logs de conteneur, la sortie d'un exec n'étant pas celle du conteneur — détail en §4.3). C'est un trou de couverture, pas de persistance. Et ce volume persistant reste, de toute façon, dans le périmètre qu'il documenterait : la perte de l'hyperviseur emporterait le cluster et tout ce que Loki contient. C'est le motif etat-critique-hors-systeme, appliqué à l'observabilité.

5.2 Axe 2 — Secrets au repos : le managé supprime le vecteur sans corriger le défaut

Le même contrôle manque des deux côtés. Ni l'un ni l'autre ne chiffre les secrets par une clé gérée par le client.

Homelab EKS
Chiffrement enveloppe par secret non (C-P) non (EK-2)
Chiffrement du stockage sous-jacent non oui, par AWS
Copies d'etcd accessibles hors du cluster oui : snapshot Proxmox, disque de VM non, le plan de contrôle est managé et inaccessible
Exploitabilité réelle haute faible

Ce que ça dit, et c'est le point le plus subtil du comparatif. Le managé n'a pas corrigé le défaut. Il a supprimé le chemin qui permettait de l'exploiter. Un audit qui compte les contrôles en échec les trouverait à égalité. Un audit qui suit les chemins d'attaque ne les trouve pas à égalité du tout.

Bonus, et c'est une jolie symétrie : les deux clusters portent le même malentendu, en sens inverse. Côté homelab, on croyait que SealedSecrets protégeait le cluster ; il protège le dépôt. Côté EKS, un commentaire affirme que les secrets ne passent pas par etcd ; ils y passent.

5.3 Axe 3 — Cloisonnement réseau : ici, l'écart ne vient pas du managé

C'est l'axe le plus honnête du rapport, parce qu'il ne flatte pas la thèse.

Homelab EKS
Namespaces avec une politique réseau 1 sur 14 6 sur 20 (corrigé à la relecture du 24/08 : inv-networkpolicies.yaml compte 6 namespaces couverts sur les 20 du cluster — fastapi-dev/staging/prod et frontend-dev/staging/prod — pas 7 sur 15)
Politique default-deny egress nulle part dans les namespaces applicatifs
Où se situe la frontière ce qui est plateforme est couvert (par le chart argocd), l'applicatif ne l'est pas ce qui est applicatif est couvert, la plateforme ne l'est pas
Origine de la couverture existante un chart Helm, personne ne l'a décidé une décision d'architecture, écrite dans le dépôt

Les frontières sont exactement inverses. Et la différence ne vient pas du fournisseur : AWS ne fournit aucune NetworkPolicy. Elle vient du travail fait.

📌 C'est la conclusion la plus utile de la section 5 : la différence entre les deux infrastructures n'est pas toujours « AWS fait mieux ». Elle est parfois « ce projet-là a été travaillé, et l'autre pas encore ».

« Pas encore » est à prendre au pied de la lettre, et c'est vérifiable dans le dépôt. La couverture réseau du homelab est l'issue #80, ouverte, séquencée après les Pod Security Standards (livrés le 10/08 sur 13 namespaces sur 14) pour une raison écrite : une politique d'admission mal réglée refuse avec un message, une politique réseau mal réglée coupe un flux en silence. Cet axe mesure donc un ordre de travaux, pas une négligence, et son verdict se renversera à la livraison de #80. Portée complète de cette limite : §1.3.8.

5.4 Axe 4 — Exposition publique : les deux clusters ne sont pas comparables ici

🔴 Correction apportée à la rédaction du 19/08. Une version antérieure de l'analyse annonçait que « les deux clusters exposent leurs consoles sur Internet », et en tirait un quatrième axe sur une habitude de conception répétée. C'est faux, et la mesure le montre.

Homelab EKS
Adresse du point d'entrée 192.168.1.201, adresse privée NLB public, 8 hosts sur devopsyouss.com
Consoles d'administration joignables depuis Internet non oui : Grafana et ArgoCD (EK-9)
Voie d'accès secondaire oui, NodePort 32475 et 31560 sur chaque nœud non
Barrière devant les consoles le réseau local d'abord, puis l'authentification l'authentification applicative seule
PT-4 joué depuis l'intérieur du cluster l'extérieur, comme il se doit

Ce que ça change, et ce n'est pas mineur :

  1. Les résultats de PT-4 ne sont pas comparables tels quels. Sur le homelab, l'authentification a été interrogée depuis un pod, après que le cloisonnement réseau a cédé. Sur EKS, elle a été interrogée depuis Internet le 2026-08-23, sans aucun préalable (détail en §4.4) : les deux consoles répondent 401. Ce ne sont pas les mêmes conditions d'attaque, et EKS a tenu dans les plus dures des deux.
  2. L'exposition d'EKS est un vrai constat isolé (EK-9), pas la moitié d'un motif partagé.
  3. Le homelab gagne un point fort qui n'était pas écrit : sa surface d'attaque externe est nulle, tant que la box ne redirige aucun port.

5.4 bis — La non-exposition du homelab, prouvée par test négatif

Mesuré le 2026-08-19, depuis une connexion mobile extérieure au réseau local, cluster en marche.

Le témoin d'abord. https://example.com répond depuis la même connexion. Sans lui, un « rien ne répond » ne distinguerait pas une absence d'exposition d'une connexion défaillante.

Cible testée Ce qu'elle couvre Résultat
https://example.com témoin répond
http://<ip-publique>/ port 80 ❌ inaccessible
https://<ip-publique>/ port 443 ❌ inaccessible
http://<ip-publique>:32475/ NodePort HTTP de MetalLB ❌ inaccessible
http://<ip-publique>:31560/ NodePort HTTPS de MetalLB ❌ inaccessible

Pourquoi les deux derniers ports comptent autant que les deux premiers. La surface exacte du cluster, mesurée le même jour :

envoy-gateway-system/envoy-...   type=LoadBalancer   ip=192.168.1.201
ports = 80:32475/TCP, 443:31560/TCP

MetalLB ouvre ces deux ports sur chaque nœud, en plus de l'adresse annoncée. Tester seulement 80 et 443 aurait laissé la moitié de la surface hors du test.

Un tunnel sortant aurait rendu le test caduc, et c'est le trou de raisonnement qu'un test entrant seul ne ferme pas. Cloudflare Tunnel, ngrok ou Tailscale exposent un service sans aucune redirection de port : la connexion part de l'intérieur, la box ne voit qu'un flux sortant ordinaire.

Vérification complémentaire Résultat
Pod de tunnel sortant dans le cluster aucun
Services hors ClusterIP un seul, la Gateway Envoy
Ingress aucun

Portée exacte de cette preuve

Ce qui est prouvé : aucun service du cluster n'est joignable depuis Internet, sur les quatre ports qui constituent sa surface, à la date de la mesure, avec témoin.

Ce qui n'est pas prouvé, et qui n'est pas revendiqué : que rien du réseau local ne soit exposé. La vérification des tunnels porte sur les pods du cluster. Un tunnel installé sur l'hyperviseur, en service systemd sur une machine virtuelle, ou sur le NAS, ne serait pas vu par cette mesure.

🔴 Cette limite n'est pas théorique, elle a un cas connu. Un tunnel Cloudflare publiant un service n8n existe sur une machine virtuelle du même hyperviseur. Il était éteint à la date de gel, il n'est pas mesuré, et il est hors périmètre : voir section 1.3.6.

Le point fort de l'axe 4 se lit donc ainsi, et pas autrement : le cluster homelab n'expose aucune console sur Internet, et non le réseau du homelab n'est pas exposé. La première phrase est prouvée. La seconde ne l'est pas, et ce rapport ne l'écrit nulle part.

C'est la même discipline que pour PT-4 : on borne l'affirmation au lieu d'étendre la conclusion.

5.5 Axe 5 — La surface irréductible que le managé déplace

Composant Homelab EKS
CNI Cilium installé et opéré, privilégié, hostNetwork, hostPath Cilium également, mais le plan de contrôle est hors périmètre
Répartiteur de charge MetalLB, hostNetwork, speaker BGP privilégié NLB AWS, aucun pod privilégié
Plan de contrôle une VM, auditable par kube-bench, non sauvegardée managé, non auditable, sauvegardé par AWS
Conteneurs privilégiés mesurés 7 ressources distinctes sur 3 contrôles, toutes légitimes 6 ressources distinctes, mêmes contrôles

Ce que ça dit. Le managé ne sécurise pas cette surface. Il la retire du périmètre du client et la déplace chez le fournisseur. Pour un auditeur, la conséquence est directe : une partie des contrôles cesse d'être évaluable, ce qui n'est pas la même chose que d'être satisfaite.

C'est exactement pourquoi la section 1.3.3 devait être écrite avant tout chiffre.

La mesure a corrigé l'hypothèse : 6 ressources distinctes, pas « bien plus bas »

Mesuré le 2026-08-23 sur les mêmes trois contrôles kubescape (C-0045 hostPath inscriptible, C-0048 montage hostPath, C-0057 conteneur privilégié) : 6 ressources distinctes en échec sur EKS, contre 7 au homelab. L'hypothèse écrite plus tôt dans la rédaction — « attendu bien plus bas » — était fausse, et il valait mieux la corriger que la laisser.

⚠️ Un second bug de décompte, trouvé à la relecture du 24/08, même motif qu'en 1.5.3. Additionner les failedResources des trois contrôles donne bien 11 (2+6+3) et 10 (2+6+2), les chiffres publiés jusqu'ici. Mais cilium et cilium-envoy échouent sur les trois contrôles à la fois et se retrouvent comptés trois fois chacun. En ressources distinctes (une ligne par objet Kubernetes, pas par ligne de résultat), le vrai compte est 7 au homelab, 6 sur EKS — vérifié en recroisant les résultats bruts de kubescape par resourceID. Compter des lignes n'est pas compter des causes, ici comme en 1.5.3. L'écart et sa cause (metallb-frr-k8s) ne changent pas : une unité, le répartiteur de charge.

La composition, ressource par ressource, explique l'écart d'une unité et rien de plus :

Ressource Contrôle(s) en échec Homelab EKS
cilium (DaemonSet) hostPath, privilégié
cilium-envoy (DaemonSet) hostPath, privilégié
alloy / collecteur de logs (DaemonSet) hostPath
node-exporter (DaemonSet) hostPath
kube-bench (CronJob homelab / Job EKS, ×2 nœuds) hostPath
metallb-frr-k8s (DaemonSet, speaker BGP) privilégié absent

🟢 La seule différence est exactement celle que la ligne « répartiteur de charge » du tableau ci-dessus prédit. metallb-frr-k8s n'existe pas sur EKS parce que le répartiteur de charge est un NLB géré par AWS, hors du cluster. Tout le reste — CNI, collecte de logs, métriques, durcissement — est un choix d'outillage identique sur les deux clusters, donc mesuré identique.

Ce que ça corrige dans le raisonnement de cet axe. Le managé retire de la surface privilégiée ce qu'il gère à la place du client (le répartiteur de charge, le plan de contrôle). Il ne retire rien de ce que le client choisit d'installer lui-même (CNI, observabilité, durcissement). La surface privilégiée n'est donc pas structurellement plus petite sur le managé : elle est réduite d'exactement ce que le fournisseur reprend, ni plus, ni moins. L'hypothèse initiale confondait « le managé gère le plan de contrôle » avec « le managé réduit la surface applicative », deux choses différentes.

Toutes légitimes des deux côtés, comme au homelab : CNI, collecte de métriques et de logs, et outil d'audit lui-même ont un besoin réel d'accès privilégié ou au disque du nœud. Aucune de ces 6 ressources ne constitue un constat en soi.

5.6 Axe 6 — Deux backends de state, deux postures

Homelab EKS
Emplacement du state Terraform MinIO sur le NAS bucket S3 <bucket-tfstate>
Sauvegardé oui, Hyper Backup oui, versioning S3 activé (s3_bucket_object_versioning, PASS)
Chiffré au repos oui, clé AES-256 hors du système oui, par défaut S3
HTTPS imposé non mesuré non (AW-5)
Suppression protégée non mesuré non, ni MFA Delete ni Object Lock (AW-5)

Deux projets du même auteur, deux backends distants, deux postures de protection différentes pour le même type de fichier critique. Il n'y a pas de raison technique à cet écart, seulement l'ordre dans lequel les choses ont été faites.

5.7 Synthèse du comparatif

Axe Qui est le mieux servi Pourquoi
1. Journalisation du plan de contrôle EKS le managé la donne en une ligne
2. Secrets au repos EKS, mais pas pour la raison qu'on croit le défaut est identique, le vecteur disparaît
3. Cloisonnement réseau EKS rien à voir avec le managé, c'est le travail fait
4. Exposition publique homelab son cluster n'est pas sur Internet, prouvé sur 4 ports avec témoin. Portée bornée en §1.3.6
5. Surface privilégiée EKS le managé la déplace hors du périmètre
6. Protection du state égalité, sur des points différents les deux sont sauvegardés et chiffrés ; EKS n'impose ni HTTPS ni protection à la suppression, non mesuré côté homelab

Quatre axes sur six au managé, un à l'auto-hébergé, un à égalité. Et surtout : sur les quatre axes gagnés par le managé, un seul relève vraiment de lui au sens technique (l'axe 5, et précisément dans la mesure de ce qu'il reprend — voir §5.5). Les autres relèvent du coût d'accès au contrôle ou du travail fait.

La conclusion à retenir n'est pas « le managé est plus sûr ». C'est : le managé rend certains contrôles moins chers à obtenir, en retire d'autres du périmètre auditable, et ne dispense d'aucune décision d'architecture.


6. Plan de remédiation

Dérivé de tous les constats acquis, homelab, compte AWS et EKS confirmés en vol.

Priorisé par le rapport entre le risque supprimé et l'effort consenti. Un plan qui liste tout dans l'ordre des constats n'est pas un plan.

Les numéros d'action sont des identifiants, pas un ordre. Ils sont attribués dans l'ordre où l'action est écrite et ne bougent plus : la priorité est donnée par la sous-section où l'action se trouve. C'est pourquoi l'action 37, ajoutée le 26/08, figure en 6.1 et non à la fin. Un numéro qu'on renumérote cesse d'être suivable, et plusieurs sections de ce rapport citent ces numéros.

Ce que ce plan engage, et ce qu'il n'engage pas. Il nomme le contrôle à obtenir et l'ordre dans lequel l'obtenir, arrêtés à la date de gel. Il ne fige ni le produit ni la méthode : ceux-là se tranchent à l'implémentation, dans le projet concerné, et s'y tracent en ADR avec les alternatives écartées. Une action reste donc ouverte à une meilleure solution tant qu'elle n'est pas livrée. Ce qui est figé, c'est le constat et sa date, pas la façon d'y répondre.

6.1 À faire en premier : coût faible, risque supprimé élevé

# Action Constat Effort Ce que ça supprime
1 Activer CloudTrail multi-région AW-1, AW-2 faible l'aveuglement total du compte. C'est le socle : sans lui, aucun autre service de détection ne sert
37 Séparer les privilèges des clés de CI du projet EKS : une clé restreinte non protégée pour ce dont les pipelines de MR ont besoin, une clé privilégiée protégée pour le déploiement. Ne pas se contenter de cocher « protégée », cela casserait les pipelines de MR (mesuré le 2026-06-10, MR !137) EK-16, AW-2 faible le fait qu'un pipeline de branche quelconque porte les identifiants du compte entier. Palliatif de l'action 34 tant qu'elle n'est pas livrée
2 Retirer secrets du ClusterRole/alloy, sur les deux clusters : c'est un défaut du chart, pas d'une installation C-Q faible un chemin direct vers tous les secrets, homelab et EKS
3 LimitRange par namespace de plateforme C-S faible la saturation mémoire d'un plan de contrôle non sauvegardé
4 Politique de bucket imposant HTTPS sur le state AW-5 très faible le transport en clair de l'état de l'infrastructure
5 Groupe de sécurité RDS sur le SG des nœuds EK-6 très faible l'accès à la base depuis toute machine du VPC
6 Retirer l'Access Entry et les Roles inutilisés de gitlab-ci EK-10 faible un privilège sans justification sur un compte à clé statique
7 Corriger les trois commentaires faux EK-2, EK-11, C-Q très faible l'égarement du prochain lecteur, y compris soi-même
8 runAsNonRoot: true sur alloy-traces C-T très faible un processus root qui n'a aucun besoin de l'être, le voisin de pod le prouve
9 Politique de mot de passe au niveau du compte AW-7 très faible un seul compte humain, mais c'est l'administrateur
10 pod-security.kubernetes.io/enforce sur external-secrets AW-11, EK-3 très faible le maillon le moins cher de la chaîne qui mène aux secrets de production
11 Politique de ressource deny by default sur les sept secrets AW-10 faible la lecture des secrets de production par tout chemin menant à AdministratorAccess
12 Retirer dev, staging et prod de la politique du rôle de contrôleur ESO AW-11 faible l'annulation du cloisonnement par environnement depuis les namespaces de plateforme
13 Activer les VPC Flow Logs vers CloudWatch, une seule ressource Terraform, aucun redéploiement du cluster EK-5 très faible l'absence totale de trace sur tout trafic réseau interne au VPC, bloqué ou autorisé
14 Répliquer la restriction des ports d'administration au niveau NACL, cohérence avec les groupes de sécurité qui la posent déjà diff CIS 5.2/5.3 très faible l'absence de défense en profondeur si la barrière des groupes de sécurité tombe
15 Poser une rétention explicite sur le groupe de logs EKS (90 jours par exemple) : aujourd'hui illimitée par défaut, ni décidée ni bornée en coût. À appliquer après la clôture de cet audit, le contenu sans terme est la preuve qui a servi à PT-7 AW-6 très faible le coût non borné d'un journal jamais purgé, sans intention derrière
16 Poser startingDeadlineSeconds sur les CronJobs kube-bench, et lire le verdict avec sa date C-A très faible un CronJob bloqué définitivement après 100 créneaux manqués sur un cluster éteint par intermittence
17 Activer ebs_encryption_by_default au niveau de la région, un réglage de compte, aucun redéploiement EK-2 très faible des volumes EBS de nœuds non chiffrés par défaut à chaque création
18 Ajouter une clause to sur la règle egress 443 (allow-fastapi), resserrée vers les points de terminaison ECR et Secrets Manager, une règle qui existe déjà PT-10 faible l'exfiltration HTTPS vers n'importe quelle destination depuis un pod compromis
19 Restreindre le solver ACME de cert-manager à un namespace dédié, les deux clusters (même chart, confirmé RE-5). Contre-épreuve : auth can-i doit répondre no sur monitoring C-V faible la suppression du Service de la Gateway par un cert-manager compromis, dans les quatorze namespaces d'un coup

6.2 Ensuite : effort moyen, risque structurant

# Action Constat Effort
20 Politique d'audit sur le kube-apiserver du homelab, sortie collectée vers Loki C-O moyen
21 default-deny ingress et egress par namespace sur le homelab, todos-demo en premier C-R moyen
22 Sauvegarde d'etcd planifiée vers MinIO, plus un test de restauration joué C-M moyen
23 Chiffrement au repos d'etcd sur le homelab C-P moyen
24 Restreindre la politique IAM de la CI aux actions réellement utilisées AW-2 moyen
25 🔴 Exiger le MFA sur iamadmin par politique IAM, avec BoolIfExists et non Bool, en excluant les actions d'enrôlement, et prouvé par l'échec de la clé brute. À appliquer après la journée live, ce compte portant Terraform et le teardown AW-9 faible
26 PSA et NetworkPolicy sur les namespaces de plateforme EKS, ferme aussi les 16 chemins d'EK-12 et le maillon 2 de C-U/C-V sur EKS EK-3, EK-12 moyen
27 Restreindre la lecture des secrets d'envoy-gateway par resourceNames ou RoleBinding par namespace EK-13 moyen
28 Restreindre l'accès aux consoles d'administration EKS par filtre d'adresse source ou accès privé EK-9 moyen
29 Rôle IRSA propre au Job de bootstrap de la base, ou sortie de ce bootstrap du cluster. Prérequis de l'action 12 : sans lui, retirer les secrets d'environnement du rôle de contrôleur casse la création des users PostgreSQL AW-11 moyen
30 Rotation du DB_PASSWORD de production, le seul secret dont la rotation automatique soit native côté RDS AW-10 moyen
31 Exiger une revue humaine sur la branche synchronisée par ArgoCD : passer son droit de push à No one sur les deux dépôts (develop côté EKS, main côté homelab), ce qui force la demande de fusion et active la condition de pipeline déjà en place (EK-15). Et sortir monitoring du niveau privileged, homelab (voir §3.5). Volet PSA d'EKS déjà couvert par l'action 26 C-U, EK-15 faible pour la revue, moyen pour le PSA
32 Ajouter une datasource CloudWatch dans Grafana, avec un rôle IRSA dédié et restreint (logs:GetLogEvents, logs:StartQuery, à ne pas fusionner avec un rôle existant vu AW-11) : ferme l'angle mort de la stack d'observabilité en cluster sur l'audit du plan de contrôle PT-7 moyen
33 Activer enable-wireguard sur Cilium pour le chiffrement est-ouest, et sortir ArgoCD du mode --insecure avec un certificat TLS interne C-Y moyen
38 Monter le plan de contrôle EKS depuis la 1.32, et poser le mécanisme qui reposera la question. Les deux gestes, pas un seul : une montée sans mécanisme reproduit le constat dans six mois. Plus : jouer la montée en place au moins une fois (voir la note ci-dessous) EK-17 moyen

Note sur l'action 38, et elle vaut au-delà de ce projet.

Reconstruire n'est pas monter de version. Détruire un cluster et le remonter depuis Terraform en version cible produit une installation neuve. Le résultat est identique, le processus n'a pas eu lieu, et rien de ce que la production impose n'a été exercé.

Ce qu'une installation neuve ne prouve pas : que le plan de contrôle se monte sans casser les charges en service ; que la rotation des groupes de nœuds respecte les PodDisruptionBudget ; que l'application reste joignable pendant le drain des nœuds ; que les modules complémentaires survivent au changement. Ce sont précisément les quatre questions d'une montée réelle.

Le caractère éphémère du cluster ne l'empêche pas, il en borne la fenêtre. Une montée en place se joue sur un cluster qui tourne, quelle que soit sa durée de vie prévue. Le geste tient dans une session : monter en 1.32, appliquer la version suivante, observer le plan de contrôle, puis les groupes de nœuds, avec l'application en service et sa disponibilité mesurée pendant la rotation, et détruire à la fin.

📌 Et l'éphémère devient ici un avantage, qu'il faut revendiquer. Une montée de version se répète autant de fois que voulu sur une infrastructure jetable, y compris jusqu'à l'échec délibéré. Aucune production ne permet de répéter ce geste. C'est le seul endroit de ce rapport où le modèle choisi fait mieux que la production qu'il imite.

⚠️ Un point à vérifier avant de planifier : EKS impose la montée une version mineure à la fois. De la 1.32 à la version du homelab, cela fait quatre paliers successifs. La preuve du processus tient dans un palier joué en place ; atteindre la cible peut ensuite se faire par reconstruction, ce qui est légitime une fois le geste exercé et documenté, et ne l'est pas avant.

6.3 Chantiers de fond

# Action Constat Effort
34 Fédération OIDC GitLab ↔ AWS, pour supprimer les cinq clés statiques au lieu de les faire tourner. Supprime aussi la question posée par EK-16 : sans clé longue durée en variable, « protégée ou non » n'a plus d'objet EK-4, AW-2, AW-3, EK-16 élevé
35 Détection à l'exécution, outil à trancher : Falco ou Tetragon (voir la note ci-dessous) C-O, PT-7, PT-8 élevé
36 Auditer et durcir le poste d'administration : c'est le point de compromission unique des deux infrastructures, et il n'est couvert par aucun référentiel C-N élevé

Note sur l'action 35. La détection à l'exécution est un chantier, pas un item d'audit. Elle est citée ici comme la réponse structurelle au trou mesuré sur les dix-neuf scénarios joués, sur les deux clusters (§0) : aucun n'a été détecté en tant qu'attaque. Ce n'est pas un correctif à appliquer dans la foulée.

⚠️ Un prérequis, valable quel que soit l'outil, et à ne pas confondre avec lui. C-O (le kube-apiserver du homelab n'a aucun flag d'audit) est un réglage du kube-apiserver (--audit-policy-file, --audit-log-path). Aucun outil de détection ne le remplace : Falco sait lire un flux d'audit d'API via son plugin k8saudit, il ne le produit pas. Sans C-O corrigé, ce plugin n'a rien à lire. L'action 35 vient après l'activation du journal, pas à sa place.

Ce plan ne tranche pas l'outil, et il dit pourquoi. Les deux candidats reposent sur eBPF et répondent au même constat par deux voies différentes :

Critère Falco Tetragon
Règles disponibles à l'installation large jeu par défaut, exploitable immédiatement peu, il faut écrire ses TracingPolicy
Portée de la réponse détecte et alerte détecte et peut bloquer dans le noyau
Lien au CNI aucun, fonctionne partout pensé pour Cilium, dont il reprend l'identité des charges
Journal d'audit de l'API le consomme (plugin k8saudit) hors de son objet

Le critère qui départage n'est pas la préférence, c'est l'objectif visé : un jeu de règles exploitable vite, ou la capacité d'empêcher plutôt que de constater. Tetragon part avec un avantage de contexte, Cilium étant le CNI des deux clusters (§2.2.1, §5.5), ce qui n'est pas une raison suffisante pour ne pas instruire l'alternative.

Ce choix se tranche à l'implémentation et se trace en ADR, pas dans ce rapport : un plan de remédiation nomme le contrôle à obtenir, le projet nomme le produit qui le fournit.

6.4 Actions de clôture de l'audit lui-même

Action Quand
🔴 Supprimer le rôle IAM prowler-audit créé pour cet audit à la clôture
Vérifier si la clé statique du profil [default], morte côté client, existe encore dans IAM à la clôture
Rejouer la correction du remote glab-base sur la seconde machine de travail à la clôture

7. Annexes

7.1 Commandes rejouables

Tout l'outillage est en conteneur, à tag épinglé. Un lecteur qui dispose du même accès peut rejouer chaque mesure de ce rapport.

kube-bench, CIS Kubernetes Benchmark

# homelab : CronJob GitOps déjà en place, un profil par type de nœud
kubectl -n kube-bench get cronjobs

# EKS : un Job par nœud, avec la tolération du nœud tainté
for n in $(kubectl get nodes -o name | cut -d/ -f2); do
  sed "s/NOM_DU_NOEUD/$n/; s/name: kube-bench/name: kube-bench-$n/" job-eks.yaml | kubectl apply -f -
done

🔴 kubectl logs job/<nom> ne rend les logs que d'un seul pod. Sur un Job à une exécution par nœud, il faut boucler sur les pods, sinon on déclare un nœud sain sans l'avoir lu.

for p in $(kubectl get pods -l app=kube-bench -o name); do
  echo "===== $p"; kubectl logs "$p" | tee kube-bench-${p##*/}.log
done

Contre-épreuve obligatoire : le nombre de fichiers produits doit égaler le nombre de nœuds.

kubescape, MITRE ATT&CK et NSA/CISA

docker run --rm --network host --user "$(id -u):$(id -g)" \
  -v ~/.kube/config:/kubeconfig:ro \
  -v "$PWD/out":/out \
  -e KUBECONFIG=/kubeconfig -e HOME=/out \
  quay.io/kubescape/kubescape-cli:v4.0.12 \
  scan framework nsa --format json --output /out/kubescape-nsa.json

⚠️ Le --user n'est pas une commodité, c'est la condition de fonctionnement. L'image tourne en uid 65532 et ne peut pas lire un kubeconfig en 600. L'outil répond alors « failed to connect to Kubernetes cluster », et ce message ment : la cause n'a rien à voir avec le réseau. On ne relâche pas les permissions d'un secret pour satisfaire un outil d'audit de sécurité.

HOME=/out est nécessaire pour que l'outil puisse écrire son cache de politiques.

prowler, CIS AWS Foundations

mkdir -p out/prowler && chmod 777 out/prowler
docker run --rm \
  --env-file ~/.aws/audit.env \
  -v "$PWD/out/prowler":/home/prowler/output \
  toniblyx/prowler:5.39.0 \
  aws --region eu-west-3 -M csv json-ocsf html -o /home/prowler/output
echo "code de sortie prowler : $?"

⚠️ Stratégies d'uid opposées entre les deux images, ne pas généraliser de l'une à l'autre.

Image uid Geste
kubescape-cli 65532 forcer --user
prowler 1000, propriétaire de son propre binaire garder son uid, ouvrir le dossier de sortie côté hôte

Avec --user sur prowler : can't open file '/home/prowler/.venv/bin/prowler': Permission denied.

🔴 Le code de sortie d'un docker run suivi d'un echo est celui de l'echo. Deux exécutions ont été rapportées en succès alors qu'elles échouaient. Capturer $? immédiatement, ou faire du conteneur la dernière commande.

7.2 Les douze points de méthode

Chacun a été payé une fois pendant cet audit. Ils sont listés parce qu'ils valent pour n'importe quel audit du même type.

# Le piège Où il a mordu
1 kubectl logs job/<nom> ne rend qu'un pod kube-bench multi-nœuds
2 filtrer un JSON kubescape sur le statut, pas sur la présence du contrôle dépouillement. Contre-épreuve : le compte extrait doit égaler le compte annoncé
3 monter le kubeconfig avec le --user de l'appelant kubescape, uid 65532
4 ne jamais afficher une valeur de secret PT-2
5 kube-bench n'audite que les nœuds de travail sur EKS périmètre, section 1.3.3
6 deux images, deux stratégies d'uid opposées kubescape contre prowler
7 sts assume-role est plafonné à 1 h depuis des identifiants déjà temporaires préparation
8 ne jamais avaler stderr dans une boucle de tentatives un refus définitif déguisé en attente a coûté douze tentatives
9 le code de sortie d'un docker run suivi d'un echo prowler, deux faux succès
10 lire un contrôle en échec par sa colonne STATUS_EXTENDED, jamais par son titre deux constats faux évités, dans les deux sens
11 ne pas conclure sur une sortie tronquée git remote -v \| head -2 a masqué le second remote
12 ne jamais comparer des scores bruts entre deux clusters section 1.5.1

7.3 Sorties brutes

Contenu Emplacement Taille
Scan prowler complet, 53 rapports de conformité ~/audit-2026-08/prowler/ 41 Mo
Scan kubescape MITRE, homelab ~/audit-2026-08/kubescape/kubescape-mitre.json 1,4 Mo
Scan kubescape NSA/CISA, homelab ~/audit-2026-08/kubescape/kubescape-nsa.json 984 Ko
Mesures Prometheus du 19/08 (mémoire, OOM) ~/audit-2026-08/homelab/raw-*.json
Inventaire homelab du 19/08, matière du schéma ~/audit-2026-08/homelab/inv-*.txt 47 Ko
Scans EKS complets (prowler, kubescape, kube-bench, blocs R/B/C, preuves du bloc D) ~/audit-2026-08/eks/ 82 Mo

Note sur inv-ciliumpolicies.txt : le fichier fait 19 octets, il contient No resources found. Un fichier vide serait une commande à rejouer, celui-ci est un résultat : il n'existe aucune CiliumNetworkPolicy dans le cluster, ce qui confirme C-R par une quatrième voie.

Hors dépôt, permissions drwx------. Ces fichiers contiennent des identifiants de compte, des noms de ressources et des adresses.


Journal de rédaction

Date Ce qui a été fait
2026-08-19 matin Première rédaction. Sections 1 à 4 pour le homelab et le compte AWS, section 5 à six axes, section 6 dérivée. Correction de l'axe 4 du comparatif, qui affirmait à tort une exposition publique du homelab. Réémission des identifiants C-H à C-K en AW-*
2026-08-19 soir (3) EK-1 confirmé par la mesure, cluster détruit et sans coût : le groupe de logs reçoit bien les flux kube-apiserver-audit, et le contenu survit à la destruction (événements du 16/08 lus le 19/08). La question la plus chère de PT-7 est tranchée avant la journée live. Nuance écrite : c'est une rétention illimitée que personne n'a choisie qui le permet, 1,02 Gio facturés sans terme
2026-08-19 soir (2) Identifiants AWS régénérés. AW-4 et AW-7 tranchés par iam:GetLoginProfile : un seul compte humain sur cinq. EK-4 révisé, trois clés déclarées contre cinq réelles, dont deux hors code d'infrastructure. Nouveau constat AW-9, critique : la clé de l'administrateur ouvre le compte sans MFA, mesuré par accident, aucune politique ne portant aws:MultiFactorAuthPresent
2026-08-19 soir Tunnel Cloudflare n8n déclaré par l'exploitant, sur une VM du même hyperviseur, éteinte au gel. Écrit en §1.3.6 comme élément hors périmètre non mesuré, sans identifiant de constat : un constat porte une criticité, et aucune criticité n'est justifiable sans mesure. L'axe 4 est reborné en conséquence
2026-08-19 après-midi (2) Axe 4 figé, prouvé par test négatif : 4 ports testés depuis une connexion extérieure, 4 inaccessibles, témoin positif, aucun tunnel sortant dans le cluster. La non-exposition du homelab passe de déduction à mesure (§5.4 bis)
2026-08-19 après-midi Homelab rallumé, trois trous soldés. C-S rechiffré par Prometheus (27,8 / 13,0 / 13,7 %, zéro OOM sur 30 jours prouvé par doublage) et nommé : le premier consommateur du cluster est kube-apiserver à 1,23 Go, sans limite. C-0013 trié : 12 faux positifs sur 23. C-Q renforcé d'un cinquième fait, alloy tourne en runAsUser: 0. Nouveau constat C-T, alloy-traces en root sans nécessité. Inventaire homelab capturé pour le schéma