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 :
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 --versionetctr 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 :
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 :
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 :
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=<nom-du-nœud>
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,
containerdoucrio). Vérifiez le fichier de service (/etc/systemd/system/containerd.serviceou/etc/systemd/system/crio.service) pour la directiveUser=. 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 avecls -l /run/containerd/containerd.sock; elles doivent êtresrw-------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éfinirNoNewPrivileges = trueet vous assurer que le profil seccomp par défaut est appliqué. Pour CRI-O, définissezseccomp_profile = "/chemin/vers/profil.json"dans la sectioncrio.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 --printdans 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é :
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 :
[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 :
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 :
sudo systemctl restart containerd
Vérifiez l'état du service :
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 :
[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 :
[crio.runtime]
conmon = "/usr/bin/conmon"
conmon_cgroup = "pod"
Après les modifications, validez :
crio config validate
Redémarrez CRI-O :
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 :
kubectl drain <nom-du-nœud> --ignore-daemonsets --delete-emptydir-data
Apportez votre modification de configuration, redémarrez le moteur d'exécution et réactivez le nœud :
kubectl uncordon <nom-du-nœud>
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 :
systemctl status containerd # ou crio
Recherchez des erreurs dans les journaux :
journalctl -u containerd -n 100 --no-pager
Pour CRI-O :
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 :
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é :
kubectl exec -it test-pod -- sh
Dans le conteneur, testez que les binaires setuid sont restreints si NoNewPrivileges est défini :
# 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 :
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 :
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 :
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 :
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 :
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 :
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 wideet 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 <nœud> --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(oucrio). - [ ] 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 <nœud>. - [ ] 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.