## Introduction Les moteurs d'exécution de conteneurs sont les logiciels qui exécutent réellement vos conteneurs sur chaque nœud Kubernetes. Si un moteur d'exécution est mal configuré, chaque charge de travail sur ce nœud est à risque. Le durcissement du moteur d'exécution est donc une tâche de sécurité fondamentale pour tout cluster Kubernetes. Ce guide se concentre sur les deux moteurs d'exécution de conteneurs Kubernetes les plus courants aujourd'hui : containerd (utilisé par Docker, GKE, AKS, EKS et bien d'autres) et CRI-O (utilisé par OpenShift et certaines distributions basées sur RHEL). Nous passerons en revue l'inventaire des versions et de l'environnement, les modifications de configuration sécurisées, la vérification, la reprise après échec, et fournirons des listes de contrôle et les pièges courants. Nous utiliserons des commandes concrètes et des extraits de configuration que vous pourrez adapter à votre environnement. Testez toujours d'abord dans un cluster de préproduction et suivez votre processus de gestion des changements. L'objectif est d'apporter des modifications observables, réversibles et limitées à la ressource visée. ## Inventaire des versions et de l'environnement Avant d'apporter toute modification, sachez exactement quel moteur d'exécution vous utilisez, sa version et comment il est configuré. Ces informations sont essentielles car les fonctionnalités de sécurité et les options de configuration varient selon la version et le type de moteur d'exécution. ### Identifier le moteur d'exécution et sa version Sur chaque nœud, vérifiez quel moteur d'exécution est actif. Vous pouvez le faire via l'API Kubernetes ou directement sur le nœud. À l'aide de kubectl, listez les nœuds et la version de leur moteur d'exécution de conteneurs : ```bash kubectl get nodes -o wide ``` Regardez la colonne `CONTAINER-RUNTIME`. Elle affichera quelque chose comme `containerd://1.6.18` ou `cri-o://1.26.0`. Sinon, connectez-vous en SSH au nœud et exécutez directement la commande de version du moteur d'exécution : - Pour containerd : `containerd --version` et `ctr version` - Pour CRI-O : `crio --version` Notez la version exacte et la version de Kubernetes (`kubectl version --short`). Consultez les notes de version et les avis de sécurité du moteur d'exécution pour votre version. Pour containerd, les vulnérabilités connues sont suivies sur sa page de sécurité GitHub ; pour CRI-O, consultez les avis Red Hat ou en amont. ### Inspecter la configuration actuelle Les deux moteurs d'exécution utilisent un fichier de configuration. Les emplacements par défaut sont : - containerd : `/etc/containerd/config.toml` - CRI-O : `/etc/crio/crio.conf` (et les fichiers de surcharge dans `/etc/crio/crio.conf.d/`) Affichez la configuration actuelle (en lecture seule) pour comprendre ce qui est déjà défini. Pour containerd, vous pouvez imprimer la configuration par défaut (qui peut différer de celle active) avec : ```bash containerd config default > /tmp/containerd-default.toml diff /tmp/containerd-default.toml /etc/containerd/config.toml ``` Cela vous montre si des personnalisations ont été apportées. Pour CRI-O, utilisez `crio --config /etc/crio/crio.conf config` pour afficher la configuration effective. Capturez l'état actuel et un horodatage. Cela sert de référence pour la restauration et le dépannage. Stockez ces informations en toute sécurité, car la configuration peut contenir des données sensibles (comme des informations d'identification de registre ou des chemins de certificats). ### Vérifier les dépendances du moteur d'exécution Le moteur d'exécution dépend du noyau, des images de conteneurs et de la kubelet. Assurez-vous que le système d'exploitation et le noyau sont à jour avec les correctifs de sécurité. Pour containerd, vérifiez que les plugins CNI sont installés et compatibles. Pour CRI-O, vérifiez que les versions de `conmon` et `runc` (ou `crun`) sont prises en charge. Utilisez les commandes suivantes pour vérifier les binaires clés : ```bash runc --version conmon --version # CRI-O uniquement cni-plugins --version # si disponible ``` ### Évaluation du rayon d'impact Avant de modifier la configuration du moteur d'exécution, comprenez quelles charges de travail s'exécutent sur le nœud. Un redémarrage du moteur d'exécution affectera tous les pods de ce nœud. Utilisez : ```bash kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName= ``` Planifiez une fenêtre de maintenance si le nœud héberge des charges de travail critiques. Assurez-vous de pouvoir drainer le nœud en toute sécurité si nécessaire. ## Chemin de configuration sécurisé Maintenant que vous avez une référence, planifiez et exécutez soigneusement les modifications de configuration. Les sous-sections suivantes couvrent les principaux domaines de durcissement pour les deux moteurs d'exécution. ### Principes généraux de durcissement - **Utiliser un utilisateur dédié pour le moteur d'exécution** : Exécutez le moteur d'exécution en tant qu'utilisateur non root (par exemple, `containerd` ou `crio`). Vérifiez le fichier de service (`/etc/systemd/system/containerd.service` ou `/etc/systemd/system/crio.service`) pour la directive `User=`. Si elle est absente ou définie sur root, ajoutez-la. - **Restreindre les permissions du socket du moteur d'exécution** : Le socket gRPC du moteur d'exécution (par exemple, `/run/containerd/containerd.sock`) ne doit être accessible qu'à root et à la kubelet. Vérifiez les permissions avec `ls -l /run/containerd/containerd.sock` ; elles doivent être `srw-------` appartenant à root. Sinon, ajustez le chemin du socket ou les permissions. - **Activer seccomp** : Seccomp restreint les appels système qu'un conteneur peut effectuer. Assurez-vous que le moteur d'exécution est configuré pour utiliser un profil seccomp par défaut. Pour containerd, dans la section `plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options`, vous pouvez définir `NoNewPrivileges = true` et vous assurer que le profil seccomp par défaut est appliqué. Pour CRI-O, définissez `seccomp_profile = "/chemin/vers/profil.json"` dans la section `crio.runtime`, ou assurez-vous que le profil par défaut est utilisé. - **Utiliser des capacités appropriées** : Limitez les capacités accordées aux conteneurs. Par défaut, les moteurs d'exécution abandonnent déjà de nombreuses capacités ; n'ajoutez pas inutilement celles qui ne sont pas nécessaires. Vérifiez l'ensemble par défaut avec `capsh --print` dans un conteneur de test. ### Configuration spécifique à containerd Modifiez `/etc/containerd/config.toml`. Après avoir apporté des modifications, validez et redémarrez le service. **Exemple : activer le plugin CRI et définir le moteur d'exécution par défaut.** Dans la config, assurez-vous que le plugin CRI est activé : ```toml version = 2 [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.9" [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] NoNewPrivileges = true SystemdCgroup = true ``` Par défaut, `NoNewPrivileges` est faux. Le définir à vrai garantit que les conteneurs ne peuvent pas obtenir de privilèges supplémentaires via des binaires setuid. C'est une étape de durcissement recommandée. **Exemple : définir le profil seccomp pour containerd** Le plugin CRI de containerd applique automatiquement le profil seccomp par défaut de Kubernetes lorsqu'il est demandé via le contexte de sécurité du pod, mais vous pouvez également définir une valeur par défaut au niveau du moteur d'exécution. Dans la section `runc.options`, ajoutez : ```toml [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] NoNewPrivileges = true SystemdCgroup = true # Le profil seccomp peut être spécifié via une annotation ou la spécification du pod. # Pour appliquer une valeur par défaut, vous pouvez utiliser un webhook de mutation. ``` Pour des valeurs par défaut seccomp plus avancées, envisagez d'utiliser les normes de sécurité des pods Kubernetes ou un moteur de politiques. Après modification, validez : ```bash containerd config dump > /tmp/containerd-new.toml containerd config verify < /tmp/containerd-new.toml ``` S'il n'y a pas d'erreurs, redémarrez containerd : ```bash sudo systemctl restart containerd ``` Vérifiez l'état du service : ```bash sudo systemctl status containerd ``` ### Configuration spécifique à CRI-O Modifiez `/etc/crio/crio.conf` ou ajoutez un fichier de surcharge sous `/etc/crio/crio.conf.d/`. **Exemple : définir l'abandon des capacités par défaut et seccomp** Dans la section `crio.runtime` : ```toml [crio.runtime] default_capabilities = [ "CHOWN", "DAC_OVERRIDE", "FSETID", "FOWNER", "SETGID", "SETUID", "SETPCAP", "NET_BIND_SERVICE", "KILL", ] seccomp_profile = "/etc/crio/seccomp.json" ``` Assurez-vous que `seccomp_profile` pointe vers un fichier de profil seccomp valide. Vous pouvez copier le profil par défaut de Kubernetes ou en créer un personnalisé. Assurez-vous également que `conmon` et `runc` sont sécurisés : ```toml [crio.runtime] conmon = "/usr/bin/conmon" conmon_cgroup = "pod" ``` Après les modifications, validez : ```bash crio config validate ``` Redémarrez CRI-O : ```bash sudo systemctl restart crio ``` ### Appliquer les modifications de configuration en toute sécurité Testez toujours d'abord sur un seul nœud. Drainez le nœud pour éviter toute interruption : ```bash kubectl drain --ignore-daemonsets --delete-emptydir-data ``` Apportez votre modification de configuration, redémarrez le moteur d'exécution et réactivez le nœud : ```bash kubectl uncordon ``` Surveillez la santé des pods sur ce nœud avant de passer aux autres nœuds. ## Vérification et diagnostics Après toute modification de configuration, vérifiez que le moteur d'exécution fonctionne correctement et que les contrôles de sécurité sont appliqués. ### Vérifier que le moteur d'exécution est en cours d'exécution Vérifiez l'état du service : ```bash systemctl status containerd # ou crio ``` Recherchez des erreurs dans les journaux : ```bash journalctl -u containerd -n 100 --no-pager ``` Pour CRI-O : ```bash journalctl -u crio -n 100 --no-pager ``` ### Tester les opérations sur les conteneurs Exécutez un pod simple sur le nœud (ou le cluster) et assurez-vous qu'il démarre : ```bash kubectl run test-pod --image=busybox --restart=Never --command -- sleep 3600 kubectl get pod test-pod ``` Vérifiez que le pod est en cours d'exécution. Ensuite, exécutez-y une commande pour tester les fonctionnalités de sécurité : ```bash kubectl exec -it test-pod -- sh ``` Dans le conteneur, testez que les binaires `setuid` sont restreints si `NoNewPrivileges` est défini : ```bash # Essayez d'obtenir les privilèges root su - ``` Cela devrait échouer si le conteneur s'exécute en tant que non-root et que `NoNewPrivileges` est vrai. Si vous êtes root, testez si vous pouvez changer d'utilisateur vers un privilège plus élevé (vous ne le pouvez pas). Testez seccomp : tentez d'utiliser un appel système non présent dans le profil par défaut, par exemple, `reboot` ou `mount` avec des arguments inhabituels. Dans un moteur d'exécution sécurisé, ils doivent être refusés. Exemple pour tester seccomp avec un profil personnalisé : créez un pod avec un profil seccomp restrictif et essayez d'exécuter un appel système interdit. ### Vérifier le contexte de sécurité du pod Assurez-vous que les pods s'exécutent avec des contextes de sécurité restreints. Utilisez : ```bash kubectl get pod test-pod -o yaml | grep -A 10 securityContext ``` Recherchez `runAsNonRoot: true`, `allowPrivilegeEscalation: false`, `seccompProfile: RuntimeDefault`, etc. Ceux-ci doivent être définis par vos politiques de pods ou vos contrôleurs d'admission. ### Surveiller les métriques du moteur d'exécution Les deux moteurs d'exécution exposent des métriques. Pour containerd, vous pouvez interroger le point de terminaison des métriques s'il est activé (généralement à `http://localhost:1338/metrics`). Pour CRI-O, les métriques sont à `http://localhost:9090/metrics`. Utilisez `curl` pour vérifier les métriques liées aux conteneurs et aux erreurs. Exemple : ```bash curl -s http://localhost:1338/metrics | grep -E 'container_|runtime_' ``` Si les métriques ne sont pas activées, envisagez de les activer pour la surveillance en production. ## Modes de défaillance et récupération Malgré une planification minutieuse, des échecs peuvent survenir. Cette section couvre les modes de défaillance courants et comment les récupérer. ### Le moteur d'exécution ne démarre pas après une modification de configuration **Symptôme** : `systemctl status containerd` affiche un échec, et `journalctl -u containerd` affiche des erreurs comme « invalid config » ou « failed to load config ». **Cause** : Erreur de syntaxe dans le fichier de configuration, option non prise en charge ou problème de permissions de fichier. **Récupération** : Revenez à la configuration précédente. Si vous avez effectué une sauvegarde (vous devriez), restaurez-la : ```bash sudo cp /etc/containerd/config.toml.bak /etc/containerd/config.toml sudo systemctl restart containerd ``` S'il n'y a pas de sauvegarde, vous pouvez utiliser la configuration par défaut générée précédemment : ```bash containerd config default > /etc/containerd/config.toml sudo systemctl restart containerd ``` Ensuite, réappliquez les modifications une par une, en validant après chacune. ### Les pods ne peuvent pas démarrer après le redémarrage du moteur d'exécution **Symptôme** : Les nouveaux pods restent dans l'état `ContainerCreating`, et `kubectl describe pod` affiche des erreurs comme « failed to create containerd task » ou « rpc error: code = Unknown desc = failed to start container ». **Cause** : Il peut s'agir de plugins CNI manquants, d'une image de sandbox incorrecte ou d'un chemin de profil seccomp incorrect. **Récupération** : Consultez les journaux du moteur d'exécution pour obtenir une erreur détaillée. Si le problème est lié au CNI, vérifiez la configuration du plugin réseau (`/etc/cni/net.d/`). Si le problème est lié à l'image de sandbox, assurez-vous que l'image pause est correcte et peut être extraite (`registry.k8s.io/pause:3.9`). Si le chemin du profil seccomp est incorrect, corrigez-le ou supprimez le paramètre seccomp et redémarrez. ### Le nœud devient NotReady **Symptôme** : `kubectl get nodes` affiche le nœud comme `NotReady` après le redémarrage du moteur d'exécution. **Cause** : La kubelet a perdu la connexion avec le moteur d'exécution, peut-être en raison d'une modification des permissions du socket. **Récupération** : Vérifiez les journaux de la kubelet : ```bash journalctl -u kubelet -n 100 ``` Si les permissions du socket ont été modifiées de manière incorrecte, restaurez les permissions d'origine (par exemple, `chmod 660` pour le socket containerd, propriétaire root:kubelet). Ensuite, redémarrez la kubelet. ### Stratégie de restauration Conservez toujours des sauvegardes versionnées des fichiers de configuration. Utilisez `etckeeper` ou des sauvegardes manuelles avec horodatage. Avant une modification, copiez la configuration : ```bash sudo cp /etc/containerd/config.toml /etc/containerd/config.toml.$(date +%Y%m%d%H%M%S) ``` Si tout le reste échoue, vous pouvez réinstaller le paquet du moteur d'exécution (cela restaurera les configurations par défaut mais vous perdrez les paramètres personnalisés) puis réappliquer votre durcissement à partir de la documentation. ## Liste de contrôle opérationnelle Utilisez cette liste de contrôle avant et après toute modification de durcissement du moteur d'exécution. ### Liste de contrôle avant modification - [ ] Identifier le type et la version du moteur d'exécution : `kubectl get nodes -o wide` et noter. - [ ] Lire les avis de sécurité et les notes de version du moteur d'exécution pour votre version. - [ ] Sauvegarder les fichiers de configuration actuels avec horodatage. - [ ] Documenter l'état actuel (config dump, état du moteur d'exécution, référence des métriques). - [ ] Identifier les charges de travail sur le(s) nœud(s) cible(s) et planifier une fenêtre de maintenance. - [ ] Drainer le(s) nœud(s) si nécessaire : `kubectl drain --ignore-daemonsets`. - [ ] Préparer le plan de restauration et les commandes de restauration. - [ ] S'assurer que la surveillance et les alertes sont actives. - [ ] Désigner un responsable pour cette modification : **Priya Shah, responsable de l'ingénierie** (ou votre équivalent). ### Liste de contrôle de vérification après modification - [ ] Le service du moteur d'exécution est actif : `systemctl status containerd` (ou `crio`). - [ ] Les journaux du moteur d'exécution ne montrent aucune erreur fatale : `journalctl -u containerd -n 50`. - [ ] Le pod de test démarre et s'exécute : `kubectl run test-pod --image=busybox --command -- sleep 60`. - [ ] Le pod de test respecte le contexte de sécurité : vérifiez `kubectl get pod test-pod -o yaml | grep securityContext`. - [ ] Le nœud est Ready : `kubectl get nodes`. - [ ] Les pods existants sur le nœud sont toujours sains (s'ils n'ont pas été drainés). - [ ] S'il a été drainé, réactiver le nœud : `kubectl uncordon `. - [ ] Nettoyer le pod de test : `kubectl delete pod test-pod`. - [ ] Mettre à jour la documentation et le référentiel de gestion de configuration. - [ ] Planifier une revue post-modification dans la semaine pour détecter les problèmes différés. ### Maintenance récurrente - **Mensuelle** : Examiner les versions du moteur d'exécution par rapport aux avis de sécurité et planifier les mises à niveau. - **Trimestrielle** : Auditer la configuration du moteur d'exécution pour la conformité aux référentiels de durcissement (CIS, STIG). - **En continu** : Surveiller les métriques et les journaux du moteur d'exécution pour détecter les anomalies. **Responsable des vérifications récurrentes** : **Alex Chen, ingénieur sécurité de plateforme** (ou votre équivalent). Le responsable examine et met à jour cette liste de contrôle chaque trimestre. ## Pièges courants Évitez ces erreurs courantes lors du durcissement des moteurs d'exécution de conteneurs. ### 1. Ne pas tester d'abord sur un seul nœud **Ce qui se passe** : Vous appliquez une modification de configuration sur tous les nœuds simultanément, provoquant une panne à l'échelle du cluster. **Pourquoi** : Excès de confiance ou pression temporelle. **Comment l'éviter** : Commencez toujours par un nœud non critique. Drainez-le, apportez la modification, vérifiez, puis déployez progressivement. ### 2. Ignorer la compatibilité des versions **Ce qui se passe** : Vous appliquez des options de configuration qui n'existent pas dans votre version du moteur d'exécution, entraînant un échec au démarrage. **Pourquoi** : Copier des extraits de configuration depuis Internet sans vérifier la version. **Comment l'éviter** : Vérifiez toujours la documentation de votre version spécifique du moteur d'exécution. Utilisez `containerd config default` ou `crio config dump` pour voir les options prises en charge actuelles. ### 3. Trop restreindre seccomp ou les capacités **Ce qui se passe** : Les applications échouent car elles ont besoin d'un appel système ou d'une capacité qui a été supprimée. **Pourquoi** : Supposer qu'un profil générique convient à toutes les charges de travail. **Comment l'éviter** : Commencez par le profil seccomp par défaut et ne supprimez des capacités que si cela est justifié. Utilisez les journaux d'audit pour identifier les appels système manquants, puis personnalisez les profils. ### 4. Oublier de sauvegarder et de tester la restauration **Ce qui se passe** : Lorsqu'une modification échoue, vous ne pouvez pas revenir rapidement en arrière, prolongeant les temps d'arrêt. **Pourquoi** : Ne pas traiter la configuration comme du code avec contrôle de version. **Comment l'éviter** : Sauvegardez toujours les fichiers de configuration et stockez-les dans un système de contrôle de version. Testez réellement la procédure de restauration dans un environnement de préproduction. ### 5. Modifier incorrectement les permissions du socket **Ce qui se passe** : La kubelet ne peut pas communiquer avec le moteur d'exécution, cassant tous les pods. **Pourquoi** : Essayer de durcir en restreignant les permissions du socket sans comprendre l'accès requis. **Comment l'éviter** : Vérifiez les permissions par défaut et le groupe requis (généralement `kubelet` ou `root`). En cas de doute, laissez les valeurs par défaut et comptez sur les permissions du système de fichiers et SELinux/AppArmor. ### 6. Négliger de mettre à jour la configuration de la kubelet **Ce qui se passe** : Les modifications du moteur d'exécution peuvent nécessiter des drapeaux de kubelet correspondants (par exemple, le pilote cgroup). **Pourquoi** : Se concentrer uniquement sur la configuration du moteur d'exécution. **Comment l'éviter** : Coordonnez les modifications entre le moteur d'exécution et la kubelet. Examinez les journaux de la kubelet après le redémarrage (`journalctl -u kubelet`). ## Conclusion Le durcissement des moteurs d'exécution de conteneurs Kubernetes est une étape cruciale pour sécuriser votre cluster. En inventoriant méthodiquement votre environnement, en apportant des modifications de configuration ciblées, en vérifiant les résultats et en disposant de plans de récupération, vous réduisez le risque d'incidents de sécurité et de perturbations opérationnelles. N'oubliez pas de tester minutieusement les modifications, de surveiller en continu et de maintenir vos versions de moteur d'exécution à jour. Commencez par un élément de durcissement à faible risque de ce guide, comme l'activation de `NoNewPrivileges` dans containerd ou la définition d'un profil seccomp par défaut dans CRI-O. Enregistrez l'état actuel, appliquez la modification sur un nœud de test et vérifiez avec les commandes fournies. Documentez tout et planifiez des examens réguliers. Au fil du temps, ces pratiques deviendront une seconde nature, conduisant à un environnement Kubernetes plus sûr et plus résilient.