1 points par GN⁺ 4 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Les attaques de la chaîne d’approvisionnement sont devenues un problème plus important à mesure que le coût de distribution des logiciels a fortement baissé et que l’automatisation du build et du déploiement s’est généralisée
  • Dans les années 1970, il y avait une crise du logiciel où il était difficile de créer des logiciels réutilisables, mais aujourd’hui les registres de paquets et les gestionnaires de paquets récupèrent et compilent du code à partir d’un simple nom et d’une version
  • Les mises à jour automatiques des dépendances permettent à des changements malveillants de se propager rapidement via la CI, et une bonne attaque de la chaîne d’approvisionnement se diffuse à la vitesse d’exécution des runners CI
  • Le vendoring, qui consiste à placer toutes les dépendances dans le dépôt du projet, alourdit le dépôt mais bloque les changements automatiques et rend plus visibles l’ampleur et le coût des dépendances
  • Ce n’est pas une solution adaptée à tous les logiciels, mais beaucoup de petits logiciels peuvent tirer profit d’une réduction à 2 ou 3 dépendances susceptibles de changer soudainement depuis l’extérieur

Problème

  • Les attaques de la chaîne d’approvisionnement deviennent un problème de plus en plus grave non pas parce que la nature du logiciel ou de sa maintenance aurait changé, mais parce que le modèle de coût du partage et de la distribution des logiciels est devenu extrêmement bas
  • Le coût de distribution a tellement baissé qu’on recourt massivement à l’automatisation même quand cela introduit du gaspillage, et l’automatisation reste utile en soi
  • Tous les quelques mois, une nouvelle attaque de la chaîne d’approvisionnement survient et finit par endommager une grande partie du code mondial

Comment en est-on arrivé là

  • À la fin des années 1960 et au début des années 1970, on ne savait pas très bien comment créer des logiciels réutilisables, et on appelait cela la crise du logiciel
  • La demande en logiciels augmentait de façon exponentielle, mais la capacité à produire de nouveaux logiciels adaptés à la complexité demandée progressait plus lentement
  • Cette période a mené à des recherches sur la modularité, la programmation structurée et d’autres sujets similaires, et presque tous les systèmes de modules des langages de programmation créés après 1990 peuvent remonter leur lignée jusqu’à Modula-2
  • Dans les années 1990 et 2000, Internet a apporté une solution plus puissante, le build et la distribution des logiciels sont devenus peu coûteux, et une grande partie des logiciels réellement désirables étaient open source
  • À partir de CPAN, CTAN et des distributions Linux, de nombreux registres de paquets et gestionnaires de paquets sont apparus, et ces outils trouvent, récupèrent et compilent des logiciels à partir d’un fichier manifeste, d’un nom et le plus souvent d’un numéro de version arbitraire
  • De l’intégration manuelle aux dépendances automatiques

    • Autrefois, une bonne manière de construire des systèmes logiciels complexes consistait à assembler soigneusement à la main des composants qui fonctionnaient, et c’est essentiellement ce que font les distributions Linux
    • En 2003, compiler SDL avec toutes ses fonctionnalités pouvait demander plusieurs jours tant c’était pénible, et il n’est pas nécessaire d’être nostalgique de cette époque
    • Lorsqu’une distribution Linux fournit un environnement de base connu, beaucoup de logiciels sur mesure peuvent fonctionner dans leur propre univers sans trop se soucier du reste du système
    • Lorsqu’ils communiquent avec d’autres logiciels, c’est souvent via des fichiers ou des sockets réseau utilisant des protocoles bien connus
    • De plus en plus de bons logiciels sont construits from scratch en Rust ou en Go, ou sont déployés dans des conteneurs Docker, et ces logiciels interagissent très peu avec les bibliothèques système
    • Plutôt que de s’aligner sur l’ensemble logiciel fourni par la distribution de l’OS, il est devenu courant que le système de build récupère lui-même les bibliothèques nécessaires
  • Une crise en sens inverse

    • Aujourd’hui, à l’inverse des années 1970, on connaît une crise où l’on réutilise trop de logiciel, au point de détériorer les programmes
    • Distribuer des logiciels reste extrêmement bon marché, mais utiliser un logiciel a toujours un coût
    • Pendant longtemps, le coût principal a été la complexité nécessaire pour compiler le logiciel et le faire tourner sur une machine, mais ce problème a largement disparu grâce à l’automatisation
    • On construit, déploie et utilise désormais beaucoup plus de logiciels, et le coût se manifeste sous forme d’enfer des dépendances, d’obésité logicielle, de temps de build prolongés, ou de disparition de paquets et de gestionnaires de paquets
    • Le plus gros problème est celui des attaques de la chaîne d’approvisionnement
  • La structure de propagation des attaques de la chaîne d’approvisionnement

    • Les attaques de la chaîne d’approvisionnement sont un problème aussi ancien que les logiciels open source eux-mêmes
    • Dans le passé, une tentative de patch malveillant visant à introduire uid = 0 à la place de uid == 0 dans le noyau Linux a été le premier patch noyau malveillant observé dans la nature, et cela relevait d’une tentative d’attaque de la chaîne d’approvisionnement
    • Si les attaques de la chaîne d’approvisionnement sont devenues plus massives et plus problématiques au cours des dix dernières années, c’est parce que les systèmes de build ont été automatisés pour récupérer le code source et le distribuer
    • Les systèmes de CI s’exécutent généralement à chaque modification de code ou lors des changements importants, et ces changements deviennent automatiquement disponibles pour toutes les personnes qui dépendent de ce code
    • Les systèmes de CI du côté dépendant récupèrent à leur tour ces changements et incluent le nouveau code malveillant, et une bonne attaque de la chaîne d’approvisionnement se propage comme un feu de forêt à la vitesse d’exécution des runners CI
    • Il existe des moyens de ralentir les attaques de la chaîne d’approvisionnement, comme les périodes de cooldown des dépendances, mais cela ouvre des débats sur les politiques et les responsabilités

Solution

  • L’idée centrale est de ne pas laisser des systèmes de build comme npm ou cargo récupérer automatiquement les dépendances depuis un emplacement réseau à chaque fois, mais de placer toutes les dépendances avec le logiciel lui-même
  • On vendorise toutes les dépendances dans le projet, en copiant le contenu du contrôle de source amont dans le dépôt git puis en le commitant
  • Lorsqu’une mise à jour amont arrive, il suffit de la retélécharger et de la recopier, puis, si les opérations manuelles deviennent fastidieuses, de laisser les outils de build l’automatiser
  • S’il existe déjà un lockfile, il suffit de le relier à l’arborescence complète des sources dans le contrôle de source
  • On possède ainsi le code en exerçant un contrôle fort sur chaque ligne de code source
  • Coûts et compromis

    • Le dépôt grossit, mais l’espace disque est peu coûteux
    • Le coût de transfert est moins négligeable que celui du disque, mais dans cette discussion il reste un facteur qu’il faut accepter
    • Le temps de build semble devoir augmenter, mais ce n’est pas forcément le cas puisqu’il fallait déjà recompiler ces dépendances de toute façon
    • La réutilisation du code peut devenir plus difficile, et cela peut être un vrai problème pour des programmes comme des clients et serveurs qui utilisent des bibliothèques de protocoles partagées
    • Ces programmes ont déjà des problèmes d’incompatibilité de versions et doivent de toute façon les gérer, donc le fait d’obliger à y prêter attention n’est pas forcément pire sur le long terme
  • Un pare-feu contre les attaques de la chaîne d’approvisionnement

    • Si les dépendances ne sont pas mises à jour automatiquement, alors chaque paquet de l’écosystème devient un pare-feu contre les attaques de la chaîne d’approvisionnement
    • La même approche bloque aussi la propagation des correctifs et des patchs, mais si un correctif est important, quelqu’un ira de toute façon le chercher manuellement
    • Les correctifs que personne ne va chercher ne sont souvent pas importants
    • On peut obtenir un effet similaire en abandonnant dans le système de build le semver ou l’idée que « deux codes différents doivent se comporter de la même manière », et en traitant tous les numéros de version comme des identifiants uniques sans relation entre eux
    • Le problème du semver est qu’il exprime une intention humaine plutôt qu’une réalité effective, et qu’il ne fonctionne même alors que lorsqu’il est utilisé à peu près correctement
    • Traiter les numéros de version comme uniques ne résout pas les problèmes de dépendances qui disparaissent, sont altérées ou dont le contenu est endommagé d’une autre manière
  • Visibilité des dépendances

    • Vendoriser toutes les dépendances ne fait pas seulement ralentir les changements automatiques, cela augmente aussi légèrement le coût d’utilisation des dépendances
    • Cette hausse de coût n’est pas irrécupérable ; elle pousse simplement à réfléchir un peu plus avant d’utiliser du code amont
    • C’est un mécanisme doux qui conduit à se redemander, lors de l’ajout d’une nouvelle dépendance, si elle est vraiment nécessaire
    • La visibilité des dépendances augmente, et l’obésité cachée derrière elles est moins dissimulée
    • Si l’on ajoute une bibliothèque simple qui semble faire environ 200 lignes et qu’elle en contient en réalité 50 000, il devient plus clair qu’il faut s’arrêter et se demander pourquoi
    • Le caractère magique des dépendances diminue, et il devient plus facile de suivre le chemin qui relie un bug dans la base de code au code de quelqu’un d’autre
  • Arbre de dépendances et problème du partage

    • Vendoriser tout par défaut peut conduire à des arbres de dépendances plus plats et plus larges
    • Il n’est pas souhaitable d’atteindre le niveau de bibliothèques géantes comme Boost ou Qt en C++
    • Si de telles bibliothèques géantes existent, c’est parce qu’il est trop difficile de créer et d’utiliser de petites bibliothèques C/C++
    • L’idée sous-jacente est qu’il vaut mieux qu’un intégrateur système comme une distribution Linux fasse ce travail une seule fois, plutôt que chacun doive comprendre lui-même comment compiler quelque chose comme Boost ou Qt
    • Le véritable inconvénient est que les dépendances transitives ne sont pas partagées
    • Si la lib A et la lib B dépendent toutes deux de Z, la déduplication n’est pas impossible, mais elle devient plus difficile et demande soit une intervention humaine, soit des outils plus sophistiqués
    • Même lorsque les dépendances transitives sont partagées, cela pose aussi des problèmes, et le simple fait d’avoir des dépendances transitives fait partie du problème
    • Autoriser des bibliothèques à déclarer des dépendances transitives revient à céder à d’autres une partie du contrôle sur son programme

Analyse

  • Tous les logiciels ne peuvent pas adopter cette méthode
  • Vendoriser puis compiler l’intégralité de Redis dans le cadre du déploiement d’un backend web n’est pas particulièrement raisonnable
  • Toutefois, si le déploiement est déjà automatisé via Ansible ou des images Docker, il est possible que l’on fasse en pratique quelque chose de très proche
  • Il existe une limite à la complexité que cette approche peut supporter, mais les entreprises à monorepo géant comme Google et Facebook montrent que cette limite peut être plus élevée qu’on ne l’imagine
  • À un certain point, les dépendances rencontrent le système d’exploitation, et le système d’exploitation est lui-même une grosse dépendance avec ses propres nombreux problèmes
  • L’idée de l’unikernel pour les backends web est séduisante, mais les outils posent encore problème et on n’en est pas encore là
  • Distributions Linux et environnements de build

    • Cette méthode n’est pas une manière de construire un système interactif complet comme une distribution Linux ou un BSD
    • De tels systèmes relèvent d’un autre problème, car ils contiennent de nombreux programmes et bibliothèques qui doivent fonctionner ensemble
    • Pousser ce principe jusqu’au bout rapproche d’approches comme Nix ou Guix
    • L’idée qu’il faudrait assembler correctement un « environnement de build » ressemble surtout à une manière paresseuse et insuffisante de résoudre la question « comment construire le logiciel »
    • Cette idée est un vestige de l’époque où l’on compilait une fois un logiciel sur un certain mini-ordinateur avant de le partager largement sous forme binaire
    • Aujourd’hui, on compile à la volée bien plus de logiciels qu’au cours des années 1970
  • Champ d’application

    • Cette approche n’est pas une solution universelle, mais elle peut s’appliquer à beaucoup de logiciels et leur apporter des bénéfices
    • La plupart des logiciels sont petits, et les grands projets doivent déjà résoudre une grande partie de ces problèmes
    • Il existe de nombreuses bibliothèques qui ne font que du calcul pur ou qui ne touchent l’extérieur qu’à travers des entrées/sorties de base et portables comme des fichiers et des sockets réseau
    • Des bibliothèques de compression, libcurl, des bibliothèques TUI ou des exemples comme Django peuvent être traités comme des cibles de vendoring
    • Le vendoring permet d’éviter dans une large mesure que le déploiement ou le build sur un nouveau système casse de manière incompréhensible à cause de conflits de versions ou de bugs introduits par des patchs soudains
    • L’objectif est de réduire non pas à 200 ou 300, mais au plus à 2 ou 3 le nombre de dépendances susceptibles de changer de l’extérieur sans préavis

Conclusion

  • Réduire les mises à jour automatiques des dépendances et faire en sorte que le projet possède aussi les sources de ses dépendances peut ralentir la propagation automatique des attaques de la chaîne d’approvisionnement
  • Augmenter légèrement le coût d’usage des dépendances et en améliorer la visibilité permet de repérer plus facilement la réutilisation inutile et l’obésité cachée
  • Cette approche ne convient pas à tous les systèmes, mais elle présente des avantages pratiques pour les petits logiciels et de nombreuses bibliothèques

1 commentaires

 
GN⁺ 4 시간 전
Avis sur Lobste.rs
  • Le gestionnaire de paquets de Zig me semble être un compromis assez correct
    Tous les paquets sont épinglés par un hash de contenu, donc c’est comme avoir un lockfile par défaut, ce qui évite le problème où « le dépôt amont devient soudainement malveillant », tout en laissant subsister le problème où « le dépôt amont disparaît »
    Cela dit, il y a à la fois un cache global et un cache local, et comme tout repose sur des hashes de contenu, si le dépôt amont disparaît, il suffit de déposer le tarball de la copie locale là où il faut
    Cela semble être un bon compromis entre « vendoriser les sources » et « un logiciel simple et réutilisable »

    • On pourrait sans doute étendre cette approche à tous les logiciels, et ce serait assez élégant
      Mettre toutes les sources dans un stockage adressé par le contenu, puis hasher chaque programme à partir du hash de ses entrées
    • Je suis globalement d’accord, mais je me demande un peu comment on pourrait attaquer cette configuration
      Il faudrait probablement modifier le lockfile ou trouver une collision de hash, et aucun des deux ne semble facile
      Cela dit, comme je suis habitué à l’écosystème cargo, ça ne me plaît pas totalement. Quand on met à jour une dépendance, ses dépendances transitives ont tendance à être mises à jour aussi sans avertissement particulier, tout comme d’autres éléments correspondant à la plage de versions sémantiques
  • Je ne considère pas ça comme une « attaque de la chaîne d’approvisionnement », puisqu’il n’y a pas de contrat signé avec offre et contrepartie, donc ce n’est pas vraiment une chaîne d’approvisionnement
    Cela dit, du point de vue consistant à garantir que les dépendances ne changent pas sous vos pieds, un lockfile avec des hashes ou la sélection de version minimale de Go revient au même que le vendoring des dépendances
    Je comprends la différence de friction avec le vendoring, mais poussé à l’extrême, on finit par tout réimplémenter soi-même ou, pire, par fabriquer des dépendances avec du code généré à la volée, donc je pense qu’il vaut mieux utiliser des logiciels écrits par des experts du domaine et bien vérifiés
    J’ai travaillé là-dessus chez Facebook, et je ne recommanderais à personne leur gestion des dépendances tierces. Pour les dépendances directes d’un crate Rust donné, tout fbsource n’autorise au plus que deux versions simultanées incompatibles au niveau semver. Mettre à jour une dépendance signifie assumer la charge de mettre à jour l’ensemble de fbsource
    C’est peut-être adapté à Facebook, mais ça ne me semble ni particulièrement excellent ni vraiment durable

    • Je suis curieux de savoir pourquoi c’est « au plus deux ». Est-ce pour permettre une migration progressive d’une ancienne version vers une nouvelle ?
      Je soupçonne que le fait que ce ne soit « ni particulièrement excellent ni vraiment durable » tient davantage à l’échelle qu’à la politique elle-même. Autoriser plusieurs versions crée un autre problème : dans la plupart des langages modernes, sauf TypeScript, on utilise principalement ou exclusivement des types nominaux, donc à chaque changement cassant, la réutilisation des types entre versions est bloquée à moins d’utiliser le « semver trick »
      Pendant Log4Shell, je me souviens nettement que les entreprises ayant beaucoup de versions dispersées un peu partout ont eu plus de mal à faire les mises à jour que celles qui avaient peu de versions ou des versions figées
    • Oui, dans ce cas on peut appeler ça une attaque de dépendance <3
  • D’après The Third Networking Truth, « avec assez d’élan, même les cochons volent. Mais ce n’est pas forcément une bonne idée »
    Beaucoup de pratiques citées chez Google/Facebook ne fonctionnent que parce que ces entreprises peuvent y injecter assez d’élan
    Par exemple, je sais que certaines de ces entreprises affectent à la prise en charge de leurs monorepos et de leurs choix en matière de dépendances une équipe plus grande que l’effectif total de l’entreprise où je travaille. Eux peuvent se le permettre, mais pour la plupart d’entre nous, c’est difficilement envisageable

  • C’est un bon point de vue. Je suis tout à fait d’accord avec l’idée que « vendoriser toutes les dépendances augmente le coût d’utilisation des dépendances »
    En revanche, il ne faut pas copier-coller libcurl. C’est une stratégie correcte pour la plupart des bibliothèques, mais c’est un mauvais conseil pour les programmes C qui traitent des entrées hostiles. Vous ne ferez pas mieux que le système d’exploitation pour garder libcurl sécurisé
    Un point auquel je n’avais jamais pensé, c’est qu’il est au moins un peu étrange que des gestionnaires de paquets pour utilisateurs finaux comme apt soient apparus avant les gestionnaires de paquets au niveau du langage
    Je pense que cela a causé beaucoup de problèmes dans la pratique. Quand on regarde rubygems au début des années 2000, il est assez clair qu’on essayait de faire un « apt pour Ruby » avec installation globale par défaut, et non une gestion par projet. Il a fallu des décennies et l’ajout de bundler pour réparer les dégâts de cette erreur, alors que si l’on avait reconnu dès le départ la nécessité de l’isolation par projet, bundler n’aurait pas été nécessaire
    Python est toujours en train de remettre de l’ordre dans cette confusion, et Perl probablement aussi, mais je ne connais pas assez le sujet

    • Donc il y a bien une limite :-) le plus difficile étant de savoir exactement où tracer la ligne
      Historiquement, les gestionnaires de paquets étaient d’abord une manière de construire des systèmes, et ces systèmes avaient plusieurs utilisateurs, des environnements de bureau et beaucoup de logiciels devant fonctionner ensemble
      Construire des logiciels coûtait beaucoup de temps et de mémoire, et il y avait énormément de logiciels par rapport à l’espace disque et à la RAM, donc la réutilisation des bibliothèques était importante
      Avec l’essor des applications web, la plupart des ordinateurs importants sont devenus des serveurs qui n’exécutent qu’un petit nombre de programmes toute leur vie, et les disques ainsi que la RAM sont devenus assez bon marché pour que la taille des binaires compte moins
      Les outils conçus pour fabriquer des systèmes n’ont pas vraiment suivi ce changement d’époque, si bien que la plupart des gens qui développent des logiciels ont surtout besoin d’outils pour bien produire un programme unique, et non un immense système interconnecté rempli de bibliothèques partagées
      Il y a aussi en parallèle toute l’histoire du « C n’a pas de vrai système de modules », mais c’est moins important ici
  • Je me trompe peut-être, mais j’ai l’impression qu’un inconvénient est que, s’il y a un bug dans une dépendance copiée, les scanners risquent de ne pas le détecter
    Dans ce cas, un problème potentiel pour lequel vous auriez normalement reçu une alerte pourrait rester silencieusement présent

    • Vu le nombre de faux positifs que produisent ces scanners, cela peut aussi être un avantage
      Les scanners sont très utiles pour montrer ce qui pourrait poser problème, mais ils deviennent vraiment pénibles quand ils forcent à repousser le travail prévu pour corriger quelque chose qu’ils croyaient être un problème alors que ce n’en était pas un
  • Si l’on suit la proposition en intégrant toutes les dépendances dans le logiciel, en copiant puis en commitant la gestion des sources amont dans un dépôt git, et en laissant l’outil de build automatiser cela quand la manipulation manuelle devient pénible, est-ce qu’on ne finit pas simplement par refaire un tour complet pour inclure du logiciel tiers sans vraiment le regarder ?

    • En continuant la lecture, il est dit qu’on peut obtenir le même effet dans le système de build en abandonnant l’idée des versions sémantiques ou du fait que « deux codes différents devraient se comporter pareil », et en traitant tous les numéros de version comme mutuellement uniques et sans rapport
      Mais cette approche ne résout pas les problèmes de disparition ou d’altération des dépendances, ni le cas où quelqu’un modifie autrement le contenu d’un paquet. C’est davantage une optimisation et, à mon avis, une optimisation prématurée. On pourra peut-être y venir un jour, mais cela ne devrait pas être le point de départ