🟢 VERSION ASSAINIE (décision D8), dérivée de
rapport-audit.mdparassainir.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îneAW-2reste 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ôlesiamadmin,gitlab-cietprowler-audit, sans lesquels la chaîneAW-2ne 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éeRE-5), mais l'authentification applicative a effectivement bloqué l'accès testé enPT-4, alors queC-Udé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 :
- 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.
- 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.
- 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) |
| Où | 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ù
#80est 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 :
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/clusterDatapuis meurt. Le CLI estkubescape-cli.kubescape-cli:latestrend la v3.0.46 alors que la dernière version est la v4.0.12. Le taglatestne pointe pas la dernière version.toniblyx/prowler:5.40.0n'existe pas. Le taglatestcontient bien 5.40.0, mais le dernier tag versionné publié est5.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.
- Le groupe existe, ce que le dépôt seul ne prouvait pas.
- Il reçoit réellement, et notamment le journal d'audit de l'API : les flux
kube-apiserver-auditsont là.enabled_cluster_log_typesn'est pas resté une intention. - 🔴 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_encryptionen é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 : activerebs_encryption_by_defaultau 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 :
- Activer CloudTrail, pour que le chemin devienne au moins visible. C'est le geste le moins coûteux et le plus rentable.
- Restreindre la politique
<projet-eks>-terraform-ci-policyaux actions réellement utilisées par les jobs Terraform. - 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 podssans 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.
- Poser
pod-security.kubernetes.io/enforcesurexternal-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. - Retirer
dev,stagingetprodde la politique du rôle de contrôleur. Il n'a besoin que des quatre secrets de plateforme. Effort : faible. - 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 :
alloyest unDaemonSet, donc présent sur les trois nœuds.- Il monte
/var/logenhostPath, donc il lit le disque de l'hôte. - Il tourne dans
monitoring, le seul namespace non-système en PSAprivileged. - Il peut lire tous les secrets de tous les namespaces.
- 🔴 Il tourne en root, explicitement. Mesuré le 2026-08-19 :
runAsUser: 0est é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.2 — le 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-identity → arn: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, pluscilium-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 RBACRE-2(EK-12). L'écart est signalé plutôt que corrigé en silence :cilium-secretsreste é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-systemsans 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
LimitRangeniResourceQuotanulle 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é :
- 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.
- 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. - 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 :
- 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.
- 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.
- 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-identity → InvalidClientTokenId |
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 :
- Est-ce que ça a été bloqué ?
- Est-ce que ça a été détecté ?
- 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
NotFoundexplicite. - 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
prunechange 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 configurerprune» : 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 :
- 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é.
- 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 (
#80NetworkPolicy, 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 :
- 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. - L'exposition d'EKS est un vrai constat isolé (EK-9), pas la moitié d'un motif partagé.
- 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
failedResourcesdes trois contrôles donne bien 11 (2+6+3) et 10 (2+6+2), les chiffres publiés jusqu'ici. Maisciliumetcilium-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 parresourceID. 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 |