
🌐 Aussi en: English · Deutsch · Español
Même soirée, autre couche de la pile. 🥱➡️🤓 Après l’article sur kube-bench, une question de suivi très raisonnable est arrivée dans la section commentaires qui n’existe que dans ma tête : « D’accord, mais qu’en est-il de la vraie machine Linux sous tout ce bazar Kubernetes ? » Bonne remarque. kube-bench ne regarde jamais que le cluster – flags de l’API server, RBAC, sécurité des pods. L’hôte AlmaLinux 9 sur lequel tout cela tourne n’a jamais eu droit au même traitement. Donc, encore un petit détour nocturne. 🐧🔍🧐 OpenSCAP et le CIS AlmaLinux 9 Benchmark, en bref
OpenSCAP est, pour les distributions Linux, l’équivalent de ce que kube-bench fait pour Kubernetes – en plus ancien, plus établi, et adossé à un véritable standard officiel (SCAP, Security Content Automation Protocol) plutôt qu’à la checklist très tranchée d’un seul éditeur. Red Hat fournit un paquet nomméscap-security-guide qui regroupe CIS, DISA STIG, PCI-DSS, HIPAA et une pile d’autres profils de conformité sous forme de contenu XCCDF lisible par machine, et oscap xccdf eval passe en revue chaque règle du profil choisi et vous répond PASS, FAIL ou NOTAPPLICABLE. Même esprit que kube-bench : des centaines de contrôles pointilleux et précis – permissions de fichiers, paramètres du noyau, configuration PAM, options de montage. Pas un scanner de vulnérabilités, mais un auditeur de configuration. 🛠️ Je l’ai lancé avec le profil CIS AlmaLinux OS 9 Benchmark, Level 1 – Server, d’abord en lecture seule (--profile, pas de --remediate), parce que, contrairement à une VM de test jetable, cette machine sert en ce moment même le blog sur lequel vous lisez ces lignes, et je voulais avoir une vue d’ensemble avant de toucher à quoi que ce soit en production. 🙏🚦 Premier round : 151 réussis, 113 échoués
293 contrôles au total : ✅ 151 PASS, ❌ 113 FAIL, ⚪ 27 N/A, ⚠️ 2 ERROR. Un ratio nettement moins bon que sur la couche Kubernetes – logique, personne n’avait jamais pointé un CIS Benchmark sur cet hôte précis, alors que le côté k3s avait déjà eu droit à un passage complet. 113, c’est beaucoup à trier à la main, alors voici la visite guidée : ce qui a été corrigé, ce qui a reçu un refus net et motivé, et – parce que c’est apparemment comme ça que se passent mes soirées désormais – les deux moments vraiment intéressants du genre « attends, pourquoi ça vient de me mentir ? » en cours de route. 🕵️1️⃣ 🗃️ AIDE – la surveillance d’intégrité des fichiers que je n’avais bizarrement jamais eue
J’ai rkhunter pour les rootkits et ClamAV pour les malwares, mais rien qui remarquerait si quelqu’un modifiait discrètement/etc/passwd ou remplaçait un binaire système. AIDE (Advanced Intrusion Detection Environment) comble exactement ce trou : il prend une fois l’empreinte du système de fichiers, puis compare régulièrement avec cette référence et donne l’alerte si quelque chose d’inattendu a changé. Installé, base initiale construite (un parcours complet du système de fichiers, ~30 secondes, ~51 000 fichiers pris en empreinte), vérification quotidienne planifiée via cron. 📅 Petit accroc pour obtenir une référence propre : mes deux premières tentatives signalaient toutes les deux 9 279 entrées supprimées, toutes dans le répertoire des modules d’une ancienne version précise du noyau – alors que ce répertoire existait bel et bien toujours sur le disque. L’explication, une fois la piste remontée : cette session SSH tourne avec ProtectKernelModules=yes, appliqué par le durcissement systemd de sshd lui-même (oui, le même durcissement que j’avais documenté pour ce blog précis il y a des mois), ce qui fait paraître /usr/lib/modules complètement vide depuis une session SSH interactive – une illusion privée et sandboxée, pas la réalité. J’avais construit la base correctement (en m’échappant de la sandbox via systemd-run), mais je l’avais ensuite inspectée avec un simple aide --check lancé directement dans ma session SSH, qui voyait la fausse version vide et paniquait à propos de fichiers « manquants » qui n’avaient jamais vraiment disparu. 🙈 Une fois la vérification relancée de la même manière « échappée » que l’initialisation, le résultat était propre : zéro différence. La leçon dépasse largement AIDE : si une vérification lancée via SSH signale un jour quelque chose de bizarre sur les modules du noyau ou /proc/sys, demandez-vous si vous ne regardez pas l’idée que la sandbox se fait du système de fichiers avant d’y croire.2️⃣ 🧱 Durcissement réseau au niveau du noyau – et le sysctl auquel je n’ai délibérément PAS touché
Ajout de la batterie standard : rejeter les redirections ICMP, refuser les paquets à routage par la source, journaliser les paquets « martiens » aux adresses source impossibles, ignorer les échos ICMP en broadcast (protection contre les attaques smurf), activer les SYN cookies TCP, limiterptrace aux propres descendants d’un processus, ASLR complet. Tout est ennuyeux, tout est sûr, tout est déployé. ✅ Ce que je n’ai pas touché, exprès, après avoir vérifié d’abord : net.ipv4.ip_forward et le filtrage de chemin inverse (rp_filter). CIS veut le forwarding désactivé et un filtrage de chemin inverse strict. Mon cluster a besoin exactement du contraire – Flannel et Calico ont besoin du forwarding IP pour pouvoir router le trafic des pods tout court, et j’ai trouvé le rp_filter de cet hôte déjà réglé sur 0 au lieu de la valeur par défaut de la distribution, 1, très probablement parce que le filtrage de chemin inverse strict est une manière bien documentée de faire jeter silencieusement du trafic de pods légitime par un nœud Kubernetes. Basculer l’un ou l’autre « pour la conformité » aurait été le moyen le plus rapide de mettre tout le cluster hors ligne pour une case à cocher. J’ai laissé les deux tranquilles, et je veux dire vraiment testé-et-vérifié tranquilles : après l’application du reste du lot sysctl, un cycle complet de sysctl --system a brièvement remis à 1 les valeurs rp_filter par interface des interfaces CNI malgré tout (un fichier par défaut de la distribution qui reprenait le dessus), alors j’ai immédiatement lancé un vrai aller-retour pod → pod → base de données via un CronJob actif pour vérifier que rien n’était cassé. Résultat propre. J’ai gardé un œil dessus ; je n’ai rien eu à annuler, mais je voulais les preuves avant d’écrire cette phrase, pas seulement la théorie. 🧪3️⃣ 🔒 Verrouiller /tmp, /var/tmp et /dev/shm – et le script d’installation que ça a cassé
Les trois ont été remontés en bind avecnodev,nosuid,noexec – rien ne devrait exécuter de binaires depuis un répertoire temporaire accessible en écriture à tout le monde. Avant de basculer l’interrupteur, je suis parti à la chasse à tout ce qui, dans mon propre code Ansible, exécute un fichier situé dans /tmp, parce que c’est exactement le genre de chose que noexec casse en silence. J’en ai trouvé un : le bootstrap de l’installateur k3s télécharge le script d’installation de get.k3s.io dans /tmp/k3s-install.sh, puis l’exécute directement par son chemin. L’exécution directe a besoin du bit exec sur le point de montage ; lire le même fichier en argument de sh, non. Correctif d’une ligne (sh /tmp/k3s-install.sh au lieu de /tmp/k3s-install.sh) avant d’activer noexec, pour qu’une future installation neuve avec ce même playbook ne casse pas discrètement sur une machine que je ne surveille même pas. Vérifié après le changement : lecture/écriture dans /tmp toujours OK, kubectl apply -f /tmp/whatever.yml toujours OK (le YAML est lu comme des données, pas exécuté), et un script de test exécutable déposé exprès a reçu un « Permission denied » net et correct. 🎯4️⃣ 🪵 journald, core dumps, permissions cron/at, durcissement de sudo, Bluetooth, rsync
Le lot peu glamour mais facile : journald écrit désormais de façon persistante sur le disque (compressé, plafonné à 500 Mo pour que « persistant » ne devienne pas discrètement « remplit le disque »), les core dumps sont désactivés partout où ils pourraient être générés (un core dump n’est qu’un instantané de la mémoire, et sur une machine qui fait tourner une base de données et une page de connexion d’administration, c’est un instantané qui pourrait contenir un mot de passe en clair en plein vol – pas quelque chose que je veux voir traîner, même en local), les répertoires cron/at ont reçu des permissions plus strictes et une liste d’autorisation explicite, sudo journalise désormais dans son propre fichier, exige un pty et redemande l’authentification à chaque fois au lieu de la mettre en cache. Bluetooth a été masqué (un VPS n’a pas de matériel Bluetooth, le service tournait simplement pour rien), etrsync a été désinstallé après avoir réellement vérifié – pas un seul script, tâche cron ou tâche Ansible sur cet hôte n’y fait référence. Si j’en ai de nouveau besoin un jour, il est à un dnf install près. 🧹5️⃣ 🎭 Les points SSH qui étaient déjà vrais, et celui auquel systemd-run n’a pas survécu
Voici le plus amusant. Plusieurs contrôles SSH sont revenus en FAIL pour des réglages que j’étais à peu près sûr d’avoir déjà corrects – restrictions de connexion root, authentification basée sur l’hôte, options d’environnement. J’ai vérifié deux fois, puis trois, la vraie configuration active de sshd viasshd -T (la commande qui montre ce que sshd appliquerait réellement, pas seulement ce qui est écrit quelque part), à la fois depuis une session SSH ordinaire et depuis une session échappée via systemd-run, pour écarter une nouvelle illusion de sandbox comme celle d’AIDE plus haut. Résultats identiques et corrects les deux fois. Ce n’était donc pas un mensonge de la sandbox – c’était en fait un écart d’un tout autre genre : OpenSSH adopte déjà par défaut le comportement sûr pour quelques-uns de ces points, mais je n’avais jamais écrit la directive explicitement, et un scanner de conformité strict vérifie le fichier de configuration à la lettre, pas « bon, la valeur par défaut se trouve être correcte ». J’ai ajouté les trois lignes manquantes directement – ceinture et bretelles, et une future mise à jour d’OpenSSH qui changerait une valeur par défaut ne modifierait plus ma posture de sécurité sans que je m’en aperçoive. Un point que j’ai délibérément laissé tel quel : CIS veut PermitRootLogin no – l’accès SSH en root complètement bloqué, point final. J’utilise PermitRootLogin without-password – uniquement par clé, mais toujours en root. Il n’y a pas de compte d’administration séparé avec sudo sur cette machine ; Ansible se connecte directement en root. Désactiver entièrement la connexion root sans d’abord créer un utilisateur d’administration dédié aurait été un moyen extrêmement efficace de me verrouiller hors du seul accès à un serveur que je suis le seul à administrer. Noté dans le code, laissé tranquille. Et la note de bas de page vraiment technique pour ceux qui voudraient reproduire cette démarche : lancer le scan OpenSCAP complet lui-même enveloppé dans systemd-run --wait --pipe (pour esquiver la sandbox SSH, même astuce que pour le correctif AIDE) échouait systématiquement avec « Remote peer disconnected » dès que je passais en arrière-plan la commande SSH qui l’avait lancé. Meilleure hypothèse : la session DBus de systemd-run se retrouvait liée à la session de connexion que PAM avait créée pour cette connexion SSH, et dès que je m’en détachais (même avec un simple nohup + disown au niveau du shell), le lien DBus mourait avec elle. Les tâches systemd-run de longue durée et le « je lance et je m’en vais via SSH » ne font apparemment pas bon ménage. Je l’ai contourné en vérifiant individuellement la poignée de règles concernées avec des appels systemd-run de courte durée, au lieu de forcer tout le scan de plusieurs minutes à passer par là.🤷 Ce que j’ai délibérément laissé de côté
Même principe que dans l’article sur kube-bench : un résultat rouge n’est pas automatiquement une tâche à faire, et rien de ce qui suit ne donne à un attaquant quoi que ce soit qu’il n’avait pas déjà :- 🔐 PAM / authselect / qualité des mots de passe / politique de verrouillage des comptes – le plus gros bloc de FAIL restants, et celui auquel je m’abstiens le plus délibérément de toucher pour l’instant.
authselecta signalé « no existing configuration detected » sur cet hôte, ce qui signifie que PAM n’a jamais été placé sous sa gestion ici. L’activer maintenant implique de réécrire en profondeur/etc/pam.d/system-authetpassword-auth– si je me trompe, je pourrais bloquer toute connexion locale, ainsi quesuetsudo, sur un serveur pour lequel je n’ai aucun accès confirmé à une console de secours. SSH lui-même fonctionne déjà uniquement par clé, donc le gain de sécurité réel est modeste face à un risque franchement catastrophique. Ça aura droit à sa propre fenêtre de maintenance soigneusement testée, pas à une intervention à la va-vite un jeudi soir. - 🔑 Mot de passe du chargeur d’amorçage GRUB2 – une mesure contre l’accès physique à la console, qui ne s’applique pour l’essentiel pas à un VPS loué et risque d’entrer en conflit avec les propres outils de console de secours de l’hébergeur.
- 🔏 Politique crypto personnalisée à l’échelle du système pour CIS – change les algorithmes TLS/SSH que toute la machine accepte. Un vrai risque de casser la compatibilité avec quelque chose (un vieux client qui consulte le blog, mes propres outils) sans tests dédiés au préalable. Pas une intervention à la va-vite un jeudi soir non plus.
- 📡 systemd-journal-remote – CIS veut qu’il soit installé pour la centralisation des journaux. Je n’ai rien de configuré pour recevoir ces journaux. Installer un démon que rien n’utilise ne durcit rien du tout, ça ne fait qu’agrandir la surface d’attaque sans aucun bénéfice.




