Pourquoi les builds reproductibles échouent souvent
- Accueil
- Développement, applications & chaîne logicielle
- Pourquoi les builds reproductibles échouent souvent
Pourquoi les builds reproductibles échouent souvent
Section intitulée « Pourquoi les builds reproductibles échouent souvent »Un build reproductible produit exactement les mêmes octets à partir des mêmes sources, quel que soit l’environnement et le moment de l’exécution. Ce critère paraît austère, mais il transforme une livraison logicielle en objet vérifiable plutôt qu’en résultat décoratif d’une chaîne CI (Continuous Integration).
Un build reproductible ressemble à une recette qui doit donner le même gâteau, même si elle est préparée dans deux cuisines différentes. Cela sert à vérifier que personne n’a discrètement modifié le résultat entre l’écriture de la recette et sa dégustation, y compris le chef très enthousiaste avec ses ingrédients mystères.
→ Lire l’explication complète de la marmotte
Déterminisme binaire : la vraie définition du build reproductible
Section intitulée « Déterminisme binaire : la vraie définition du build reproductible »Comparer deux fichiers par leur version, leur taille ou leur comportement ne suffit pas. Un build est reproductible si deux exécutions indépendantes génèrent des artefacts bit à bit identiques : même archive, même binaire, même manifeste, même image de conteneur. La comparaison doit porter sur une empreinte SHA-256 (Secure Hash Algorithm 256 bits), pas sur un test fonctionnel.
Le compromis central est net : on gagne une capacité de vérification indépendante et une détection fiable des dérives de chaîne logicielle, mais on paie par un enfermement volontaire des entrées de build. Répertoires de paquets mouvants, compilation dépendante du réseau, tags d’images et horloge système deviennent incompatibles avec cette promesse. La recommandation est de viser la reproductibilité pour tout artefact déployable ou signé, pas pour chaque prototype local. Cette règle reste valable tant que la chaîne peut figer ses dépendances et conserver les sources nécessaires.
Un hash identique ne prouve pas l’absence de vulnérabilité. Il prouve que deux constructeurs ont obtenu le même résultat. Un compilateur compromis peut produire le même binaire malveillant partout. La reproductibilité réduit donc le risque de substitution et d’altération non détectée, elle ne remplace ni revue de code, ni séparation des rôles, ni gouvernance cryptographique.
La première erreur consiste à confondre « build qui passe deux fois » et « build déterministe ». Une exécution répétée sur le même agent cache l’état local : cache de dépendances, fuseau horaire, fichier de configuration oublié, version implicite du compilateur. Vingt ans de métiers de build apprennent que le fichier absent de Git est toujours présent sur l’agent qui sert de référence. Il attend seulement le vendredi soir pour disparaître.
Entrées cachées : temps, dépendances et environnement d’exécution
Section intitulée « Entrées cachées : temps, dépendances et environnement d’exécution »Le code source n’est qu’une entrée parmi d’autres. Le graphe réel inclut le compilateur, l’éditeur de liens, les bibliothèques système, les dépendances transitives, les variables d’environnement, le répertoire de travail, l’encodage, la locale, le fuseau horaire et la date. Un binaire peut incorporer le chemin absolu de compilation, une archive peut stocker l’heure de modification de chaque fichier et un générateur peut trier selon la langue active.
SOURCE_DATE_EPOCH fournit un instant de référence sous forme de timestamp Unix. Les outils compatibles l’utilisent pour neutraliser les dates injectées dans les archives, binaires et métadonnées. Sa présence ne rend rien reproductible par magie : elle fixe une source de variance connue. Un outil qui ignore cette variable conserve sa petite liberté artistique, généralement dans l’en-tête d’un fichier qu’aucun test ne regarde.
Les dépendances doivent être résolues par contenu, pas par nom mutable. Un tag Git, une version flottante telle que latest, ou un intervalle de version est une référence, pas une identité. Un fichier de verrouillage doit contenir les versions exactes et, lorsque l’écosystème le permet, les sommes de contrôle des archives téléchargées. Le miroir interne doit préserver ces octets, sinon il ne sert qu’à déplacer l’incertitude derrière un pare-feu.
L’environnement doit être hermétique : pas d’accès sortant pendant la compilation, sauf étape explicitement dédiée à l’acquisition et à la vérification des dépendances. Cette séparation empêche qu’un registre compromis fournisse une archive différente entre deux builds. Elle facilite aussi l’analyse d’incident : une dépendance consommée est connue, archivée et vérifiable.
Les prérequis minimaux sont les suivants :
- une version figée de chaque outil de compilation et de packaging ;
- un fichier de verrouillage vérifié en revue ;
- un dépôt d’artefacts conservant les dépendances par empreinte ;
- une image de build référencée par digest, pas seulement par tag ;
- une horloge et une locale neutralisées ;
- un stockage des sources, des dépendances et des scripts permettant une reconstruction hors du système de CI.
Une image OCI (Open Container Initiative) identifiée par digest garantit le contenu des couches référencées. Elle ne garantit pas que le moteur de build, le noyau ou le montage de volume n’ajoutent aucune différence. Le conteneur réduit la surface de variance, il ne constitue pas une capsule de vérité tombée du ciel.
Exemple de paquetage déterministe avec GNU tar
Section intitulée « Exemple de paquetage déterministe avec GNU tar »L’exemple suivant produit une archive tar.gz stable à partir d’un répertoire dist/ déjà compilé. Il requiert GNU tar 1.32 ou plus récent, GNU gzip, une locale disponible et un arbre de fichiers dont les contenus sont déjà déterministes.
#!/usr/bin/env bashset -euo pipefail
# Fixer les entrées d'environnement qui influencent le tri et les dates.export LC_ALL=Cexport TZ=UTCexport SOURCE_DATE_EPOCH=1704067200
# Refuser un répertoire de sortie absent plutôt que de créer un paquet vide.test -d dist
# Trier les noms, fixer propriétaire, groupe et date de chaque entrée.find dist -print0 \ | sort -z \ | tar --null --files-from=- \ --format=posix \ --sort=name \ --mtime="@${SOURCE_DATE_EPOCH}" \ --owner=0 --group=0 --numeric-owner \ --pax-option=delete=atime,delete=ctime \ -cf - \ | gzip -n > application.tar.gz
# Conserver l'empreinte attendue pour une vérification indépendante.sha256sum application.tar.gz > application.tar.gz.sha256gzip -n supprime le nom de fichier et la date habituellement placés dans son en-tête. --sort=name évite que l’ordre d’énumération du système de fichiers décide du résultat. Les options de propriétaire empêchent les identifiants locaux d’être encodés dans l’archive. Sans elles, deux agents parfaitement configurés peuvent diverger parce qu’un compte système a reçu un identifiant différent. Oui, l’identité numérique d’un utilisateur peut casser un paquet. Le secteur a connu des mystères plus glorieux.
Cette commande ne corrige pas les octets produits dans dist/. Une minification JavaScript dépendante de la version de Node.js, un générateur de code incluant un chemin local ou un compilateur injectant une date doivent être corrigés en amont. Le test final consiste à reconstruire depuis deux environnements frais et à comparer application.tar.gz.sha256.
Attaques de chaîne de build et limites de la comparaison
Section intitulée « Attaques de chaîne de build et limites de la comparaison »Le build reproductible traite d’abord les substitutions silencieuses. Un attaquant qui modifie un paquet téléchargé, empoisonne un cache ou change une variable de compilation provoque une divergence détectable si une reconstruction indépendante existe. Cette indépendance est la condition décisive : faire deux builds sur le même agent, avec le même cache compromis, produit deux mensonges identiques.
Les contre-mesures prioritaires sont le téléchargement des dépendances par hash, la séparation entre acquisition et compilation, des exécutants de build éphémères et des rebuilds réalisés par une infrastructure distincte. L’exécutant doit recevoir des jetons à durée courte et à privilège minimal. Un secret de production n’a rien à faire dans une étape qui compile du code. Le faire entrer parce qu’un test d’intégration « en a besoin » est le début d’un roman policier dont la fin coûte cher.
La signature d’artefact ajoute une identité au résultat, pas une preuve de reproductibilité. Une clé de signature stockée dans un HSM (Hardware Security Module) limite l’extraction de la clé privée, mais elle n’empêche pas un pipeline autorisé et compromis de signer un artefact frauduleux. La signature doit donc être liée à l’empreinte de l’artefact, aux sources exactes, au fichier de verrouillage et à l’identité de l’exécution.
La reproductibilité ne garantit pas non plus :
- que les sources sont saines ;
- que le compilateur ne contient pas de porte dérobée ;
- que les dépendances sont exemptes de vulnérabilités ;
- que le déploiement utilise réellement l’artefact vérifié ;
- que les secrets utilisés par les tests n’ont pas fuité.
Le reste doit venir de contrôles de revue, d’analyse de dépendances, de politiques de déploiement et de gestion des secrets. Les sujets de développement sécurisé et de menaces se rejoignent ici : l’intégrité d’un artefact ne compense pas une identité de pipeline sur-privilégiée.
Exploitation : rotation, conservation et reprise de build
Section intitulée « Exploitation : rotation, conservation et reprise de build »Un système reproductible doit pouvoir reconstruire un artefact ancien après la rotation des outils et des clés. Conserver seulement le binaire est insuffisant. Il faut archiver les sources, les fichiers de verrouillage, les images de build par digest, les dépendances et les métadonnées de compilation. La durée de conservation dépend de la politique de maintenance et des obligations de traçabilité, mais elle doit dépasser celle des caches éphémères. Un cache n’est pas une archive, même lorsqu’il porte un joli nom de plateforme.
Les clés de signature demandent une rotation planifiée. Une rotation ne doit pas rendre les signatures antérieures illisibles : conserver les certificats publics, l’historique de révocation et les métadonnées d’horodatage nécessaires à la vérification. La clé privée ne doit jamais être exportée pour « dépanner » une migration. Si l’architecture impose cette exportation, l’architecture a déjà choisi son problème.
La supervision doit distinguer trois alertes : une divergence de hash entre reconstructeurs, une dépendance introuvable dans le dépôt interne, et une signature émise hors du pipeline autorisé. La divergence mérite une investigation, pas un relancement automatique jusqu’à ce que les hashes coïncident. Réessayer un calcul non déterministe revient à consulter une boule de cristal avec plus de processeurs.
Le plan de reprise doit documenter comment reconstruire sans accès au registre public, comment vérifier les signatures après compromission d’une clé, et comment invalider un artefact déjà promu. La continuité dépend aussi de sauvegardes testées des dépôts de dépendances et de clés publiques, sujet indissociable de la stratégie de sauvegarde et de reprise.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans déterminisme binaire, aucun artefact trouvé en production ne peut être rapproché d’un commit : la seule réponse disponible est une présomption, jamais une comparaison d’empreintes. L’analyse d’un incident bascule alors vers la rétroingénierie du binaire, opération lente et coûteuse, là où une reconstruction aurait tranché en quelques minutes. Un inventaire de dépendances (SBOM) publié à côté du paquet devient une déclaration invérifiable, puisque rien ne prouve qu’il décrit les octets livrés. La recherche de la révision fautive par bissection se pollue de différences parasites, chaque build ajoutant sa propre variance au diagnostic. Un tiers, client, auditeur ou distributeur, perd toute possibilité de contrôle indépendant et doit accepter l’artefact sur parole. Enfin, une divergence introduite par un cache empoisonné reste indétectable faute de valeur de référence à confronter.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Pour fabriquer un programme, on part de fichiers écrits par des développeurs et on utilise des outils qui les transforment en fichiers exécutables ou distribuables. Un résultat reproductible exige que deux fabrications séparées donnent exactement le même fichier, octet par octet.
Cela impose de contrôler toutes les entrées. Les versions des outils doivent être fixées. Les bibliothèques téléchargées doivent être connues, conservées et vérifiées par leur empreinte numérique. La date, l’heure, la langue du système, le dossier de travail et l’ordre des fichiers doivent être neutralisés lorsqu’ils peuvent modifier le résultat. Une image servant à fabriquer le programme doit aussi être identifiée par son contenu exact, pas par une étiquette qui peut changer.
Après la fabrication, une empreinte numérique est calculée sur le fichier produit. Une autre machine peut refaire la fabrication avec les mêmes sources et comparer les deux empreintes. Si elles diffèrent, une entrée a changé ou un outil agit de façon variable. Il faut alors trouver la cause, pas relancer jusqu’à obtenir de la chance.
Cette méthode détecte des modifications cachées dans les dépendances ou dans la chaîne de fabrication. Elle ne prouve pas que le programme est sans défaut, ni que les outils ne sont pas compromis. Les clés servant à signer les fichiers doivent être protégées, renouvelées et surveillées. Les sources, outils et dépendances doivent enfin être conservés, sinon il devient impossible de refaire une ancienne fabrication lorsque survient un incident.
À lire aussi
- Vérifier une chaîne de compilation reproductibleFondations de la compilation reproductible, des attaques de build et de la vérification moderne de la chaîne de compilation.
- SBOM pour inventorier les composants réellement exécutésUne SBOM permet d’identifier les composants qui composent réellement un logiciel, à condition de la produire au bon endroit et de l’exploiter.
- Images Docker de base et confiance héritéeGérer la généalogie des images de base Docker pour maîtriser la confiance, les vulnérabilités et l'exploitation.
- Signer commits et artefacts contre quelles attaquesSigner ses commits et ses artefacts : contre quoi, précisément, avec les limites, prérequis et règles d’exploitation à appliquer.
Pages voisines
La citent
- Sécuriser les mises à jour automatiques de dépendances et semver
- SLSA : niveaux Build, provenance in-toto et vérification
- Formats, bibliothèques et API : rendre la crypto utilisable
- AI Act : qui contrôle, Bureau de l'IA, Comité IA et autorités nationales (articles 64 à 70 et 74)
- Attaques supply chain et protection des dépendances
- Écrire un Dockerfile durci avec condensé, multi-étapes et non root
- L'attaque de la chaîne d'approvisionnement logicielle en une page
- Vérifier une chaîne de compilation reproductible
Pourquoi les builds reproductibles échouent souvent
Elle cite
