Une image de conteneur durcie, point par point
Utilisateur non-root, système de fichiers en lecture seule, SBOM, signature : ce que chaque mesure empêche réellement, dans quel ordre les appliquer, et ce qu’elles ne remplacent pas.
7 min de lecture
Une image de conteneur est une archive qui contient une application et tout ce dont elle a besoin pour tourner : bibliothèques, outils système, parfois un shell complet. Plus l’image contient de choses, plus il y a de choses à mettre à jour, à auditer, et à exploiter si quelqu’un prend le contrôle de l’application. Durcir une image consiste à retirer ce qui est inutile et à restreindre ce qui reste.
Voici les mesures qui comptent, dans un ordre qui va du plus rentable au plus avancé, avec pour chacune ce qu’elle empêche concrètement. Aucune ne remplace une application sans faille, mais chacune réduit ce qu’un attaquant peut faire s’il en trouve une.
1. Partir d’une base minimale
Une image de base généraliste embarque un système complet : gestionnaire de paquets, shell, utilitaires. Chaque composant ajoute des vulnérabilités potentielles et des outils pratiques pour un intrus, qui n’a plus à apporter les siens. Les bases minimalistes, comme les images dites distroless ou une base Alpine, ne contiennent que le nécessaire à l’exécution.
Combiné à un build en plusieurs étapes, cela donne une image finale qui ne contient ni compilateur, ni outils de build, ni code source des dépendances. On construit dans une première étape, on copie uniquement le résultat dans la seconde.
2. Ne pas tourner en root
Par défaut, de nombreux conteneurs exécutent leur processus en tant qu’administrateur du conteneur. Ce n’est pas équivalent à être administrateur de la machine, mais c’est un point de départ trop favorable pour une évasion ou pour la modification de fichiers que le processus ne devrait pas toucher.
La parade consiste à déclarer un utilisateur numérique non privilégié dans l’image, avec l’instruction USER, par exemple USER 10001. Un identifiant numérique est préférable à un nom, car Kubernetes peut vérifier qu’il est différent de zéro. Du côté du cluster, on ajoute runAsNonRoot: true dans le contexte de sécurité pour refuser tout démarrage qui contredirait cette règle. Le processus doit aussi écouter sur un port supérieur à 1024, ce qui évite d’avoir besoin de privilèges particuliers.
3. Un système de fichiers en lecture seule
Si le système de fichiers racine du conteneur ne peut pas être modifié, un intrus ne peut pas y déposer un outil, remplacer un binaire ou modifier une configuration. Avec Kubernetes, on active readOnlyRootFilesystem: true.
L’application doit alors écrire ailleurs. C’est là que la plupart des mises en œuvre échouent la première fois : le programme veut écrire dans /tmp, dans un dossier de cache, dans un répertoire de données. La solution est de monter explicitement des volumes pour ces seuls chemins, souvent un volume temporaire emptyDir pour ce qui est jetable, et un volume persistant pour les données. On découvre ainsi précisément où l’application écrit, ce qui est utile en soi.
4. Retirer les privilèges inutiles
Linux découpe les pouvoirs de l’administrateur en capabilities. Un conteneur en reçoit un lot par défaut, dont la plupart ne servent à rien pour une application ordinaire. Dans le contexte de sécurité, on supprime tout, puis on rajoute au cas par cas ce qui est nécessaire, ce qui est rare :
capabilities.drop: [ALL];allowPrivilegeEscalation: false, qui empêche un processus d’obtenir plus de droits que son parent ;- un profil
seccompde typeRuntimeDefault, qui bloque les appels système les plus dangereux et rarement utiles.
Ces réglages se font côté déploiement plutôt que dans l’image, mais une image bien conçue est celle qui les supporte sans broncher.
5. Ne pas laisser de secrets dans les couches
Une image est faite de couches empilées, et chacune est conservée. Un mot de passe copié puis supprimé dans une étape suivante reste lisible dans la couche d’origine. Les secrets ne doivent donc jamais entrer dans l’image : ils arrivent à l’exécution, par des variables d’environnement ou des fichiers montés par l’orchestrateur. Un fichier .dockerignore bien tenu évite aussi d’embarquer par mégarde un dossier de configuration local ou un historique Git.
6. Épingler ce qu’on utilise
Une image de base référencée par un tag mouvant, comme node:22, n’est pas la même d’un mois à l’autre. Pour reconstruire à l’identique et savoir exactement ce qui tourne, on la référence par son digest (@sha256:…), et on met à jour de manière délibérée, avec un outil qui propose les changements plutôt qu’en les subissant. On épingle aussi les versions des paquets installés quand c’est possible.
7. Scanner, sans se noyer
Des outils comme Trivy ou Grype comparent le contenu de l’image avec des bases de vulnérabilités connues. Lancé dans la CI, un scan évite de livrer une image avec une faille critique déjà corrigée en amont. Il faut cependant accepter qu’il produise du bruit : une vulnérabilité dans un paquet présent mais jamais appelé n’a pas le même poids qu’une autre dans le chemin d’exécution. Ce qui aide, c’est de réduire la surface (point 1) et de trier selon l’exploitabilité plutôt que de viser un compteur à zéro.
8. Le SBOM : l’inventaire de ce qui est dedans
Un SBOM, pour software bill of materials, est la liste lisible par une machine de tous les composants d’une image et de leurs versions. Les formats courants sont SPDX et CycloneDX, et des outils comme Syft ou les attestations de BuildKit savent le produire à la construction.
Son intérêt est très concret. Le jour où une faille est annoncée dans une bibliothèque, vous pouvez répondre en quelques minutes à la question « est-ce que nous l’utilisons, et dans quelles images ? » au lieu de fouiller à la main. Un SBOM n’empêche aucune attaque, il raccourcit le temps de réaction.
9. La signature : prouver d’où vient l’image
Sans signature, rien ne garantit que l’image que votre cluster tire est bien celle que votre CI a construite. Avec Sigstore et son outil cosign, on peut signer une image, et l’on peut le faire sans gérer de clé privée : l’identité de la CI, via OIDC, suffit à obtenir un certificat de courte durée, et la signature est inscrite dans un journal de transparence public, Rekor.
La signature n’a de sens que si elle est vérifiée. Côté cluster, un contrôleur d’admission, par exemple Kyverno ou le policy controller de Sigstore, refuse de démarrer toute image qui n’est pas signée par l’identité attendue. On peut y joindre des attestations de provenance, qui décrivent comment et par quoi l’image a été construite, dans l’esprit du cadre SLSA.
Une signature non vérifiée est de la décoration.
10. Ce qu’on gagne, et ce qu’on perd
Un conteneur durci est plus difficile à déboguer : sans shell, on ne peut plus s’y connecter pour inspecter. Kubernetes propose pour cela des conteneurs de débogage éphémères, qu’on attache à un pod en cours d’exécution, avec leurs propres outils. C’est un bon compromis : l’image de production reste mince, et l’outillage n’existe que le temps d’une intervention.
Voici un ordre raisonnable pour y aller sans tout casser :
- Build en plusieurs étapes et base minimale.
- Utilisateur non-root et port non privilégié.
- Rootfs en lecture seule avec volumes explicites.
- Suppression des capabilities et profil seccomp.
- Scan dans la CI, puis SBOM.
- Signature, puis vérification à l’admission.
Le durcissement ne rend pas une application sûre : une faille applicative reste une faille. Mais il transforme une prise de contrôle complète en un incident circonscrit, et c’est exactement ce qu’on attend d’une défense en profondeur.
Commentaires (0)
Aucun commentaire pour le moment.