
🌐 Aussi en: English · Deutsch · Español
🇸🇪 Me revoilà de Suède. Bronzage : inexistant (c’est la Suède). Nombre de piqûres de moustique : illégal. Envie de remettre les mains dans mon homelab : au plafond. Deux semaines de pause forcée sans écran au fond d’une forêt, ça a un drôle d’effet : on rentre à la maison et on a immédiatement envie de se connecter enssh à un cluster Kubernetes un mercredi à 23 h. Pas de diagnostic, merci. 💻 Ces derniers soirs, j’ai donc fait ce que fait toute personne parfaitement équilibrée après des vacances : 📚 je me suis plongé dans la sécurité Kubernetes, plus précisément dans le programme du Certified Kubernetes Security Specialist (CKS). Soyons clairs – je n’ai pas l’intention de passer l’examen pour l’instant, je geeke simplement sur le sujet parce qu’il est vraiment passionnant et qu’il concerne directement un cluster que je fais tourner en production (bon, « production » – c’est un blog). Le CKS, c’est le programme « d’accord, mais savez-vous vraiment sécuriser ce truc ? » du monde Kubernetes – durcissement RBAC, supply chain, sécurité à l’exécution, network policies, tout le buffet des « choses qui vont très bien jusqu’au moment où elles ne vont plus du tout ». Et quelque part au fond de ce terrier de lapin, j’ai redécouvert un outil que j’avais utilisé une fois il y a des années et dont j’avais complètement oublié l’existence : kube-bench 🔍.🧐 Qu’est-ce que kube-bench, et c’est quoi au juste un CIS Benchmark ?
kube-bench est un petit binaire Go d’Aqua Security qui vérifie la configuration de votre cluster Kubernetes par rapport au CIS Kubernetes Benchmark – une grosse checklist ennuyeuse et extrêmement utile publiée par le Center for Internet Security, qui dit des choses comme « hé, la journalisation d’audit de votre serveur API devrait sans doute être activée » ou « vos certificats kubelet ne devraient pas être lisibles par tout le monde, petit génie ». Il ne pirate rien, il ne cherche pas de CVE, il passe simplement en revue des centaines de vérifications de configuration très précises et très pointilleuses – droits de fichiers, options du serveur API, bindings RBAC, réglages de sécurité des pods – et rend pour chacune un verdict PASS, FAIL ou WARN. Ce qui est bien avec kube-bench, c’est qu’il est livré avec des profils adaptés à chaque distribution. Un cluster kubeadm standard ne ressemble pas à EKS, qui ne ressemble pas à GKE, qui ne ressemble vraiment pas à k3s (ma distribution de prédilection pour ce homelab, parce que qui a assez de RAM pour un « vrai » control plane ?). Je l’ai lancé avec le profilk3s-cis-1.9 sur le cluster k3s à nœud unique qui héberge actuellement, eh bien, ce blog précisément.🚦 Premier round : 55 pass, 4 fail, 57 warn
Le premier passage a renvoyé ✅ 55 PASS, ❌ 4 FAIL, ⚠️ 57 WARN, ℹ️ 14 INFO. Pas catastrophique, mais pas rien non plus. Petite mise au point avant d’aller plus loin : la plupart de ces 57 WARN sont des vérifications que kube-bench marque « Manual » – l’outil est littéralement incapable de les contrôler automatiquement et affiche toujours WARN, quelle que soit la qualité réelle de votre configuration, parce qu’il faut qu’un humain regarde la chose et juge par lui-même. Un gros chiffre de WARN ne veut donc pas automatiquement dire « 58 problèmes », plutôt « 58 choses qu’un humain devrait jeter un œil, dont certaines sont déjà correctes ». J’ai jeté un œil à pas mal d’entre elles. La suite plus bas. 👇 Voici le tour d’horizon de ce qui a été corrigé, de ce qui s’est révélé ne pas être un problème et – parce qu’aucun changement d’infrastructure ne survit au contact de la réalité – des deux ou trois façons vraiment drôles dont j’ai cassé des choses en en réparant d’autres. 🙃1️⃣ 🔐 Des certificats avec les droits d’accès d’un banc public
k3s écrit ses certificats PKI internes (ceux sous/var/lib/rancher/k3s/server/tls/) en 644 – lisibles par tout le monde. Le CIS veut 600. Pour être honnête, il s’agit des certificats publics, pas des clés privées (celles-ci étaient déjà correctement verrouillées), donc le risque réel relevait plutôt du « un peu brouillon » que de la « faille béante ». Corrigé avec un chmod, et comme k3s régénère parfois certains de ces fichiers au redémarrage, j’ai rendu le correctif idempotent dans le rôle Ansible, pour qu’il se réapplique à chaque exécution du playbook au lieu de revenir discrètement en arrière.2️⃣ 🎫 Des tokens ServiceAccount montés dans des pods qui n’appelleront jamais l’API Kubernetes
Un grand classique. Par défaut, chaque pod reçoit un token de l’API Kubernetes monté automatiquement dans son système de fichiers, « au cas où ». Mes pods WordPress, MariaDB et Redis ne parlent jamais à l’API Kubernetes de toute leur existence – ils servent un blog, stockent des lignes et mettent des choses en cache, respectivement. Pourtant, ils avaient tous un token API valide, bien que peu privilégié, posé dans/var/run/secrets/kubernetes.io/serviceaccount/, juste « parce que pourquoi pas ». Si un attaquant avait un jour compromis l’un de ces conteneurs (disons via une RCE dans un plugin douteux – c’est WordPress, ça arrive 😅), ce token aurait été un terrain de reconnaissance offert sur un plateau. J’ai mis automountServiceAccountToken: false partout où ce n’était pas nécessaire – sur les pods eux-mêmes et, comme je l’ai appris à mes dépens, sur les objets ServiceAccount sous-jacents, parce que la vérification automatique de kube-bench inspecte apparemment le ServiceAccount, et pas seulement si un pod se trouve le surcharger. Tant que j’y étais, j’ai appliqué le correctif à l’échelle du cluster pour tous les namespaces hors système. 🧹3️⃣ 🕵️ Plongée dans le RBAC : je m’attendais à trouver quelque chose, je n’ai rien trouvé
Le CIS contient toute une série de vérifications sur la minimisation du RBAC – permissions wildcard, qui peut usurper l’identité de qui, qui peut créer des pods, etc. J’y suis allé en m’attendant à trouver au moins un binding trop large que quelqu’un (moi, il y a six mois, à 1 h du matin 🌙) aurait laissé traîner. Que dalle. Chaque rôle personnalisé du cluster s’est révélé être soit un rôle Kubernetes par défaut totalement inutilisé (admin/edit, lié à personne), soit un rôle installé par un chart Helm (cert-manager, ingress-nginx) taillé exactement pour ce dont ce composant a besoin, et rien de plus. Vraiment satisfaisant de le confirmer plutôt que de le supposer. ✨4️⃣ 🧱 NetworkPolicies : celle que je repoussais depuis longtemps
Petit bout d’histoire du homelab : ma stack Nextcloud a des NetworkPolicies appliquées via Calico depuis une éternité, mais la stack WordPress/blog n’a jamais eu droit au même traitement. Le raisonnement à l’époque : Collabora (l’éditeur de documents de Nextcloud) a un modèle de menace évident et propre – il analyse des documents non fiables et n’a aucune raison légitime de toucher un jour à la base de données, donc le verrouiller allait de soi. Pour WordPress, c’était plus flou : le pod WordPress doit légitimement parler à MariaDB et à Redis, donc « tout interdire » n’est pas la bonne forme de politique. Mais plus flou ne veut pas dire inutile. La vraie valeur ici n’est pas d’isoler WordPress de sa propre base de données – c’est de s’assurer que MariaDB et Redis ne sont joignables que depuis WordPress, et pas depuis n’importe quel autre pod qui atterrirait un jour dans ce cluster, et de limiter le rayon d’explosion de WordPress lui-même s’il se fait compromettre. J’ai donc enfin écrit trois NetworkPolicies : WordPress peut joindre MariaDB, Redis, le DNS et l’internet ouvert sur le port 443 (pour les mises à jour des plugins et du core – j’explique dans un instant pourquoi ce dernier point ne peut pas être resserré davantage) ; MariaDB et Redis ne sont joignables que par WordPress et rien d’autre. Une limite honnête que j’avoue publiquement, parce que la cacher serait malhonnête et en plus inutile : les NetworkPolicies Kubernetes standard ne savent filtrer que par IP/CIDR, pas par nom de domaine, et mon installation Calico tourne dans le mode gratuit « policy-only », sans jeux de règles sophistiqués basés sur les domaines. Le core de WordPress, ses plugins et mon bootstrap WP-CLI ont tous besoin de HTTPS sortant vers une distribution tournante d’IP de CDN (wordpress.org, le CDN de releases de GitHub, etc.) que je ne peux tout simplement pas énumérer. Le port 443 vers l’internet ouvert reste donc autorisé. Ce que cette politique m’apporte réellement, c’est le blocage des déplacements latéraux dans le cluster – un pod WordPress compromis ne peut pas aller fouiner du côté de cert-manager ou de l’interface d’administration du contrôleur ingress –, pas un verrouillage complet du trafic sortant. Je préfère vous dire honnêtement jusqu’où va une mesure plutôt que de vous laisser croire qu’elle en fait plus qu’en réalité. 🙏5️⃣ 🐛🐛 Deux bugs qui ne sont apparus que parce que j’ai vraiment testé au lieu de me fier aux coches vertes
C’est le moment de l’article où le travail d’infrastructure cesse d’être une checklist pour devenir une véritable enquête, et honnêtement, c’est la partie qui m’a le plus amusé. 🕵️♂️ Après avoir déployé les NetworkPolicies, j’ai déclenché manuellement la tâche cron de WordPress (un CronJob Kubernetes qui exécutewp-cron.php toutes les 5 minutes) pour vérifier que rien n’était cassé. Elle a renvoyé STATUS: Complete ✅. Parfait, on livre – sauf que je suis vraiment allé lire les logs au lieu de croire le statut vert, et j’y ai trouvé une page d’erreur fatale WordPress complète : « Error establishing a Redis connection. » 💥 À chaque exécution du cron. En silence. Depuis toujours, apparemment, parce qu’un wp_die() PHP se termine avec le code 0, et Kubernetes n’a donc absolument aucune idée que quelque chose a mal tourné. « Complete » voulait juste dire que le processus s’était terminé, pas qu’il avait réussi. Leçon réapprise à la dure : un statut vert est une affirmation, pas une preuve. En creusant, il s’est avéré qu’il y avait deux bugs distincts empilés l’un sur l’autre :- 🐛 Bug n°1 – le vrai, déjà présent, sans aucun rapport avec ce que j’ai fait aujourd’hui : la définition du conteneur du CronJob n’avait jamais eu la variable d’environnement
WORDPRESS_CONFIG_EXTRA– le bloc qui définitWP_REDIS_HOSTet compagnie –, contrairement au déploiement principal de WordPress.WP_REDIS_HOSTétait donc indéfinie à chaque exécution du cron depuis toujours, le plugin de cache objet Redis se rabattait discrètement sur sa valeur par défaut127.0.0.1, et évidemment rien n’écoute sur localhost dans ce pod. Rien à voir avec les NetworkPolicies, Calico ou quoi que ce soit d’aujourd’hui – c’était cassé depuis l’écriture du CronJob, et ça n’a jamais été remarqué parce que « Complete » a menti à tout le monde, moi compris, pendant on ne sait combien de temps 🙈. Corrigé en copiant exactement le même bloc de configuration depuis le déploiement principal. - 🐛 Bug n°2 – le vraiment nouveau, causé par la NetworkPolicy que je venais d’ajouter : une fois le bug n°1 corrigé et le pod cron pointé vers le bon nom d’hôte Redis, il échouait encore – mais cette fois avec un simple « Connection refused » au lieu d’une erreur fatale WordPress, ce qui est une tout autre saveur d’échec et sentait immédiatement le problème réseau plutôt que le problème de configuration. Résultat : la toute première tentative de connexion sortante d’un pod flambant neuf, dans environ la première seconde de son existence, peut être rejetée parce que l’agent Felix de Calico n’a pas encore fini de programmer les règles de pare-feu de ce pod précis ⏱️. Les pods qui vivent longtemps ne s’en rendent jamais compte, parce que leurs readiness probes bénéficient d’un délai de grâce généreux de 20 secondes avant que quiconque ne vienne vérifier. Un pod de CronJob qui essaie de parler à Redis dans la première demi-seconde de sa vie n’a pas ce luxe. J’ai reproduit le phénomène de façon fiable avec un test isolé (simple connexion TCP, sans WordPress) : échec immédiat sur un pod tout frais, succès à chaque fois avec un sleep de 5 secondes avant. Correctif : un
sleep 5 &&collé devant la vraie commande cron. Pas élégant. Redoutablement efficace. 🛠️
6️⃣ 🤦 Celle où j’ai désactivé par accident une protection par défaut en en ajoutant une autre
Mon autogoal préféré de tout l’exercice. J’ai ajouté le plugin d’admissionEventRateLimit au serveur API pour empêcher un pod en crash loop d’inonder le flux d’événements. Kubernetes standard traite --enable-admission-plugins de manière additive – il ajoute votre plugin à ceux déjà activés par défaut. k3s, il s’avère, ne partage pas cette philosophie : définir cette option remplace entièrement sa liste par défaut. Ce qui signifie que ma modification d’une ligne, « ajoutons un peu de rate limiting », a discrètement désactivé NodeRestriction, un plugin d’admission par défaut de k3s qui empêche un kubelet compromis de trafiquer des objets API qu’il ne devrait pas toucher. Je ne m’en suis aperçu que parce que j’ai relancé kube-bench ensuite, par acquit de conscience, et vu une vérification jusque-là réussie basculer directement en FAIL 📉. Morale de l’histoire : listez toujours explicitement vos valeurs par défaut quand vous touchez à une option comme celle-ci, et relancez toujours votre outil de validation après chaque modification, pas seulement une fois tout à la fin.7️⃣ 🗝️ Les secrets en fichiers plutôt qu’en variables d’environnement
Les mots de passe des bases de données traînaient en clair dans des variables d’environnement à l’intérieur des conteneurs – lisibles via/proc/<pid>/environ, kubectl describe, des crash dumps ou n’importe quel gestionnaire d’erreurs assez bête pour afficher son environnement lors d’une erreur fatale. Les images Docker officielles de WordPress et de MariaDB savent toutes les deux lire les secrets depuis un chemin de fichier (la convention du suffixe _FILE), j’ai donc tout basculé. Le seul piège pas évident ⚠️ : les health checks de MariaDB lisaient le mot de passe root directement dans cette même variable d’environnement. Si je convertissais la variable en fichier sans toucher aux probes, MariaDB se déclarerait définitivement en mauvaise santé dès le déploiement. Repéré pendant la relecture, probes modifiées pour lire directement le fichier, testé en conditions réelles avant de considérer le travail comme terminé. 👍8️⃣ 📦 La provenance des images, version honnête
Le CIS réclame « Image Provenance using ImagePolicyWebhook » – une vraie fonctionnalité de Kubernetes, mais la mettre en place implique de monter et d’héberger tout un service webhook externe chargé d’approuver ou de refuser les images au moment de l’admission. Ça représente une sacrée dose de nouvelle infrastructure pour un blog de homelab, et je n’allais pas construire tout un microservice juste pour faire plaisir à une case de conformité 🙅. J’ai plutôt utiliséValidatingAdmissionPolicy – un mécanisme natif, intégré au serveur API (GA depuis Kubernetes 1.30, aucune pièce mobile supplémentaire) – pour rejeter tout pod du namespace WordPress dont l’image de conteneur ne figure pas sur une liste d’autorisation explicite des dépôts exacts que cette stack utilise réellement. Même objectif de contrôle, zéro nouvelle infrastructure à surveiller à 2 h du matin. 😴🤷 Ce que j’ai délibérément laissé en l’état
Tout ce qui apparaît en rouge n’a pas besoin d’être corrigé, et je veux être transparent sur ce qui reste signalé et pourquoi – parce que « on a absolument tout corrigé » est généralement le signe que personne n’a regardé d’assez près, et parce qu’aucun des points suivants ne donne quoi que ce soit d’utile à un attaquant :- 🗄️ Une vérification « etcd » qui ne me concerne pas du tout. C’est un cluster k3s à nœud unique qui utilise son datastore SQLite embarqué, pas etcd. Une vérification CIS qui demande un fichier CA etcd contrôle un composant qui ne tourne littéralement pas ici. Pas une anomalie, juste un benchmark générique.
- 🧩 Une permission RBAC wildcard qui appartient à Calico lui-même, nécessaire pour qu’il gère ses propres ressources personnalisées. C’est le prix visible de l’activation des NetworkPolicies décrite plus haut – la rogner à la main risque d’empêcher le moteur de network policies lui-même de démarrer, ce qui m’a semblé un plus mauvais compromis que « une autorisation wildcard sur un composant à qui l’on confie déjà le réseau du cluster ».
- 🔧 Une poignée de composants qui ont encore leurs tokens d’API Kubernetes montés – à savoir cert-manager, mon contrôleur ingress et les propres contrôleurs de Calico. Tous les trois ont besoin de cet accès pour faire leur travail (surveiller respectivement les certificats, les objets ingress et les network policies). Les leur retirer ne durcirait rien, ça casserait juste l’émission des certificats TLS.




