Packageless, la distribution sans paquets

Packageless, la distribution sans paquets

Paquets

Les paquets permettent d’installer des logiciels et des bibliothèques sur un ordinateur. Sur un système d’exploitation si on veut être précis.

Lac de Laffrey

Les paquets ont la bonne granularité déployer des applications et mutualiser les bibliothèques (en limitant la redondance sur le disque dur) et permettent de mettre à jour simplement la machine.

L’article se concentre sur Linux (pour les serveurs), mais ils existent des outils similaires à installer sur des OS propriétaires comme Chocolatey pour Windows ou Homebrew pour MacOS.

Formats

Archives brutes

Sur les distributions de dinosaures, comme Slackware (en 1993), de simples archives (du tar compressé par exemple) sont utilisés. Slackware oublie de gérer les dépendances, ce qui est un peu peu.

Paquets sources

Pourquoi se contenter de paquets tout prêts qui font des choix à notre place alors qu’il est possible de tout compiler avec vos options choisies avec amour ?

Gentoo propose ça.

Concrètement, l’intérêt est uniquement pédagogique, pour comprendre le fonctionnement d’un Linux.

La plupart du temps, une béquille est proposée sous forme de paquet binaire, pour gérer des monstres comme Libreoffice ou Firefox.

Paquets binaires

Les paquets binaires sont déjà compilés et s’installent beaucoup plus rapidement. Des systèmes spécifiques de paquet apparaissent, comme les .deb (techniquement dpkg, en 1994) et .rpm (en 1997). D’autres formats apparaitront plus tard.

Les paquets civilisés apportent les fonctionnalités suivantes :

  • Gestion des paquets dépendants ou recommandés
  • Gestion des conflits de paquets
  • Suppression du paquet et des paquets dépendants orphelins
  • Signature cryptographique pour ne plus craindre la falsification
  • Proposition d’une configuration par défaut
  • Redémarrage des services liés
  • Possibilité de reconstruire le paquet à partir d’un paquet source

Bibliothèques

Pour mutualiser la place (et historiquement la mémoire), les systèmes d’exploitation utilisent des bibliothèques partagées, un bout de code qui pourra être utilisé par plusieurs applications (et même d’autres bibliothèques).

La plupart des UNIX (dont Linux) utilise le standard ELF qui utilise le format shared objects, aka .so, pour ses bibliothèques dynamiques.

La commande ldd permet de lister qui est lié à quoi.

Découverte d’une faille de sécurité OpenSSL ? Hop, il suffit de mettre à jour libssl et de redémarrer toutes les applications qui l’utilisent avec un intuitif lsof +c 0 | grep libssl | awk '{print $1}' | sort | uniq ou en faisant confiance à votre gestionnaire de paquets.

Le comportement des bibliothèques est similaire pour la plupart des langages, mais les langages interprétés, mais pas ceux qui compilent en statique.

Spaghettis de liens

Avec des cascades de bibliothèques, il est possible de créer des drames grandioses, comme un CVE niveau 10 avec la tant redoutée CVE-2024-3094 qui infect liblzma utilisé par libsystemd utilisé par openssh. Les détails gores sont .

Micro-bibliothèques

Les langages modernes fournissent un ensemble de bibliothèques classiques, pour avoir un socle commun et surtout une qualité constante (indispensable pour les bibliothèques liées à la sécurité). On parle de “bibliothèque standard” ou “piles incluses”.

Pour ne pas assumer ce travail, certains langages ont fait le choix des micro-bibliothèques qui ne font pas grand-chose et devrait pouvoir être remplacé facilement en cas d’abandon.

Concrètement, chaque micro-bibliothèque dépend de beaucoup de bibliothèques. Chaque projet se retrouve alors avec une ribambelle de dépendances. La quantité est telle qu’il y aura forcément une partie des dépendances mal maintenue ou devenant incompatible.

On ne nommera pas le langage le plus enfoncé dans ce choix, mais un simple npm audit vous donnera l’état d’un de ses projets.

Paquets de bibliothèques

Emballer les bibliothèques partagées est un choix évident, et il est même possible de proposer diverses variantes, mais une seule version (ce qui est de moins en moins vrai).

Les langages interprétés utilisent, eux aussi, des bibliothèques, mais dans leur propre format.

La compilation statique (les bibliothèques sont intégrées au binaire) permet de résoudre (violemment) le souci des versions épinglées. Go et Rust compilent systématiquement leurs bibliothèques en statique, tant pis pour la taille du binaire, mais au moins, on évite les pinaillages de compatibilité de versions.

Des distributions comme Debian mettent un point d’honneur à pouvoir recompiler n’importe quel paquet sans dépendre de sites tiers. Démarche honorable, mais hors C/C++, la plupart des applications utilisent des pelletées de bibliothèques (pas forcément dans la même version), et ne corrigent les failles de sécurité que dans la version courante.

Donc, même s’il est possible d’empaqueter des applications sans dépendances, l’obligation de fournir des paquets sources des bibliothèques utilisées pour les compiler vire rapidement au drame.

Techniquement, le vendoring (copie des sources des bibliothèques dans le projet) peut résoudre la possibilité de recompiler sans dépendre de sites tiers, mais en remplissant le disque dur de multiples copies.

Paquets tiers

Certaines applications ne rentrent pas dans les clous d’une distribution, comme on l’a vu précédemment.

Les outils pour créer des paquets sont fournis avec la distribution, et pour les formats de paquet contemporains, la tache est triviale.

Le premier intérêt de fournir des paquets éditeurs et la liberté de faire des montées en versions fonctionnelles à son rythme, pas une fois par an.

La gestion des bibliothèques dans les paquets tiers est compliquée (versions, options différentes…).

Différents paquets pour différentes distributions dans différentes versions (et différentes architectures) va rapidement devenir pénible (même si une CI/CD facilitera beaucoup le travail). La création, même laborieuse, est faisable, par contre, la gestion de bugs spécifiques ou de bibliothèques dont les versions sont incompatibles, va rapidement devenir infernal. La distribution en paquets systèmes d’application avec le support technique d’un éditeur ne se fera que pour quelques distributions.

Les entrepôts de paquets tiers, fort pratique, n’ont pas forcément les mêmes performances et disponibilité qu’un entrepôt de distribution.

Les mises à jour de sécurité devraient être équivalente, mais la non garantie de figer les fonctionnalités peut amener des surprises lors des mises à jour. Chose que devrait attraper les tests (intégrations et fonctionnels) dans la CI.

Déployer une application métier dans des paquets systèmes

Il ne faut plus compiler du code métier sur le serveur de production. Rails avec sa compilation des assets l’a démontré dans la douleur.

Un artefact est créé à partir des sources de l’application, puis déployé en production. Ne vous faites pas de mal, ne déployez jamais d’application métier dans un .deb, jamais.

Cohérence des versions et mises à jour

Les distributions garantissent plus ou moins la cohérence entre les paquets. Plus pour Debian, moins pour Archlinux (et Slackware).

Debian a fait le choix de l’hyper-cohérence entre les paquets et refuse de changer la version d’un paquet dans une distribution.

Des aplications utilisent des bibliothèques dans des versions très récentes, modifiées ou compilées avec des options spécifiques.

L’exemple classique est v8, le moteur Javascript de Google connu pour ses montées en versions frénétiques. Chromium, Nodejs et Mongodb (quand il était encore libre) utilisent des v8 différents, ce qui les excluent de fait des entrepôts Debian.

Debian fait bien sûr rigoureusement les mises à jour de sécurité, mais en rapiéçant (patch) le code, pour ne RIEN changer au niveau fonctionnel, et donc de n’amener aucunes surprises aux applications qui utilisent ces paquets.

Globalement, l’engagement de stabilité fonctionnelle est tenu.

Monorepo, solution ultime pour la cohérence et la fraicheur

Le monorepo (une seule source de code versionné), principalement aux échelles titanesques de Google ou Meta, permet d’avoir une cohérence rigoriste entre les bibliothèques partagées par les différents services.

Dès que l’on pousse une modification de code, non seulement ses tests sont lancés, mais surtout, déclenche les tests toutes les applications avec la nouvelle version de la bibliothèque. La modification n’est pas validée tant que tous les tests concernés ne passent pas. Une fois validé, le déploiement continu se déclenche (progressivement, avec comparaison des métriques avant et après la mise à jour). S’il y a une régression dans une application utilisant la bibliothèque, c’est à elle de corriger (et de blinder ses tests pour la prochaine fois).

Le déploiement passera par un artefact dans tous les cas, et à une époque, Facebook le faisait en pair à pair (son artefact avait une taille déraisonnable).

Fraicheur d’une distribution

La durée de vie d’une distribution se compte en années, et bien plus pour les versions LTS (support à long terme). La fréquence de sortie de nouvelles versions d’application n’est clairement pas le même.

Il y a eut des drames avec Clamav, par exemple, qui a stoppé le support de l’ancien format de description de virus, obligeant Debian à créer un nouvel entrepôt de paquets.

Le problème se pose pour les applications, mais aussi les bibliothèques, les interpréteurs (hors Perl qui est stable, lui), les outils de développement.

Développer avec des outils vieillissants est une punition.

Les navigateurs web sont les plus agressifs sur les montées en version, contraint par les soucis de sécurité, mais aussi par les fonctionnalités. De toute façon, la course à la version se fait sur les smartphones, Windows et MacOS, ce serait dommage que Linux se fasse distancer. Les applications web sont LE système universel pour distribuer des applications, surtout avec l’arrivée de webassembly, et pour ça il faut un socle sain, homogène et performant.

Ubuntu, pragmatique, ne fournit plus de paquets .deb pour Firefox/Chromium. Enfin si, le paquet .deb se contente d’installer le navigateur avec un autre système de paquet, sacrilège pour les puritains.

Mise à jour roulante

Plutôt que de fournir des versions gravées dans le marbre tous les ans (voir plus), des distributions choisissent de ne jamais figer les versions, d’avancer continuellement.

Archlinux, une sorte de Gentoo civilisé, ne sort jamais de version, enfin, pas jamais, tout le temps, suivant le principe de mise à jour roulante (rolling update). La possibilité de faire une mise à jour n’est pas garanti, il faut les faire régulièrement pour limiter les drames.

Amateur de Debian Sid, voici un nouveau challenge.

Distribution immuable

Les distributions immuables (comme feu CoreOS) garantissent une cohérence ultime. Pas de gestion de paquets, donc ni ajout ni suppression ni mises à jour possibles. Tout est gravé dans le marbre. Pour protéger le marbre, la partition racine est montée en lecture seule.

L’image est construite à partir de paquets (pour ne pas en utiliser ensuite). CoreOS utilisait les paquets de Gentoo, et des rpm dans sa reprise par Redhat, Fedora-CoreOS.

Le choix est extrême, mais l’idée est de servir de socle pour une abstraction de plus haut niveau. Imaginé pour les conteneurs, ils peuvent aussi gérer des machines virtuelles.

Pour mettre à jour, il suffit de redémarrer sur une nouvelle version. Techniquement, il y a deux partitions bootables, la courante, en lecture seule, la version suivante sur laquelle on a appliqué des patchs, soit n et n+1. Si le redémarrage de mise à jour se passe mal, rembobinage sur la dernière version connue comme stable.

Ce type de distribution n’est pas réservée aux serveurs, il en existe des dédiées aux bureaux.

Méta-paquets

L'art d'emballer du furoshiki 風呂敷

Entre le tout paquet des distributions classiques et le sans paquets des distributions immuables, il y a une place pour des solutions plus souple.

Une application métier va s’appuyer sur un ensemble d’outils systèmes, soit un tout petit bout d’un serveur Linux.

Oui, certaines technologies isolationnistes permettent de s’en passer.

Une application dans une image ne va pas dépendre du système hôte (distribution et version), mais juste des capacités du noyau.

Environnements virtuels spécifiques aux langages

Tous les langages de script savent utiliser un environnement spécifique à l’application (et pas global). Les bibliothèques utilisées seront spécifiques à cet environnement, dans des versions précises. Tout sauf utiliser les bibliothèques empaquetées par la Distribution.

Partir d’une liste de bibliothèques dans des versions précises est l’approche recommandée par les outils qui chassent les versions trouées (comme le Dependabot de Github mais surtout l’OSV, comme évoqué dans mon billet Sécuriser Internet).

La plupart des langages utilisent une description des dépendances avec des contraintes sur les versions, puis un autre fichier qui contient les versions gelées, celles choisies lors de la dernière montée en version.

L’utilisation de bibliothèques partagées (des .so) va un peu casser l’universalité de cette solution.

Pour avoir un environnement d’exécution déterministe et donc normalisé, du poste du développeur à la production en passant par la CI, il va falloir utiliser quelque chose de plus costaud.

Images immuables pour la virtualisation

Netflix a communiqué sur sa tactique de création d’images immuables démarrées dans EC2. Celui qui n’a pas souffert avec Packer ne peut imaginer à quel point cette approche est laborieuse et pénible. Même avec Cloud-init.

Cloud-init permet de configurer une machine virtuelle au démarrage qu’elle que soit l’hébergeur.

Le kernel-less profite aussi de la (para)virtualisaiotn et réduit à l’os la taille de l’image disque, mais avec des contraintes radicales. On en a déjà parlé sur ce blog.

Images OCI (aka Docker et k8s)

L’image disque n’est pas la bonne échelle pour déployer une application (ça va mieux en le redisant).

Les conteneurs utilisent des images avec un bout d’arborescence Linux, indépendamment du choix de Distribution. L’arborescence est construite en couche d’oignons, mutualisés entre les conteneurs, avec tout en bas une image minimaliste basée sur une distribution Linux.

L’application dans un conteneur est lancé dans un contexte isolé par diverses fonctionnalités du noyau Linux.

Docker en a prouvé la faisabilité, OCI l’a normalisé, et Kubernetes l’a déployé en cluster. Redhat l’a remis en cause avec Podman, qui ne comprend pas l’intérêt d’avoir un service pour gérer les conteneurs alors qu’il y a déjà Systemd.

L’image est construite avec des paquets (Alpine, Debian…) si l’application n’est pas un gros binaire statique.

Les courageux démarrent le conteneur en lecture seule, ce qui rend le conteneur encore plus prévisible. C’est bien au niveau sécurité (pas moyen de corrompre l’application), et aussi pour les écritures intempestives (log, debug ?) qui tenteront de remplir le disque dur.

Paquets agnostique pour le bureau

Proposer des paquets systèmes pour la pléthore de distribution Linux existante est tout simplement impossible.

Par contre, reprendre l’idée des .app de MacOS (de Next en fait), en ajoutant une couche de sécurité (et une sous-couche mutualisée) permet de distribuer une application sans les contraintes de l’hôte.

Les outils de bas niveaux et les fonctionnalités du noyau sont essentiellement celles utilisées par les conteneurs.

Flatpack

Flatpak permet de distribuer des applications, même propriétaire sous Linux sans se soucier de la distribution et de sa version. L’application est dans un “bac à sable”, et demandera des droits pour agir avec l’hôte, comme le font les applications sur téléphones.

Flatpack utilise les mêmes outils d’isolation de Linux que les conteneurs. Plutôt que du Layering, Flatpack dédoublonne les fichiers avec OStree.

Il fournit un ensemble de Runtime, sur lesquels on va pouvoir compiler ses applications. Niveau paquet, on est très hardcore, ce sera le classique ./configure && make && make install.

Les runtimes sont fournis par Freedesktop, le grand réconciliateur du monde Linux. Freedesktop-sdk est construit à partie de rien, sans paquets.

Des runtimes spécifiques sont fournis, comme Gnome ou KDE, et il est possible de faire cohabiter différentes versions.

Fedora, choqué à l’idée de ne pas utiliser ses rpm a négocié l’utilisation des images OCI en plus du OSTree originel.

Snap

Plutôt que de se fatiguer à faire du lobbying comme Redhat pour Flatpack, Unbuntu a créé son Snap.

Le fonctionnement de Snao est très, très classique.

Un bout d’arborescence sur un Squashfs (système de fichier compressé en lecture seule) posé sur une Base (une Ubuntu LTS) et des droits fins pour accéder à l’hôte.

La construction de paquet est isolée par LXD (un outil de conteneur pour les ringards) et utilise des .deb.

On voit bien l’intérêt d’Ubuntu de pouvoir proposer un Firefox frais sans casser la cohérence de sa distribution, tout en s’appuyant sur son propre écosystème, mais c’est navrant de voir une dispersion entre Flatpack et Snap qui font, à quelques pinaillages près, la même chose.

Entrepôt d’artefacts

Il faut pouvoir mettre à disposition les paquets (quelque soit leur forme) que l’on souhaite utiliser. Du poste de dev à la prod en passant par la CI.

Chaque technologie expose son entrepôt en ligne (et doit avoir une facture sympa en CDN).

Il est possible de cacher ces entrepôts, au fur et à mesure, ou de créer des miroirs (s’il est centralisé). La cache des entrepôts, fort important pour l’intégration continue sera le sujet d’un autre billet.

Signatures

Pour éviter la falsification des paquets (suite à une compromission du serveur ou via un miroir menteur), il faut signer les paquets.

Techniquement, on calcule le hachage du paquet, qui est signé avec une clef asymétrique.

Chaque langage a son propre système de signature à base de clefs asymétriques et de chaines de confiance.

GPG est souvent utilisé même si le principe de la chaine de confiance est une utopie ratée. Pour pallier ça, un trousseau de clefs de confiance (qui permettent de signer les clefs des mainteneurs) est fournie avec la distribution ou avec les entrepôts tiers.

Pour généraliser tout ça et surtout pouvoir signer n’importe quel artefact sans passer par une poignée d’autorités de certification (comme le fait TLS), une nouvelle approche universelle est proposée : Sigstore (comme évoqué dans le billet Distroless).

Qu’une personne signe un paquet n’a pas beaucoup de sens. Un mainteneur peut le faire, car il est le seul responsable de ce paquet. Pour un développeur, signer un commit est logique, mais comme le code libre est en théorie réalisé à plusieurs, qui est légitime pour signer le paquet ? Un éventuel “dictateur bénévole à vie” qui valide toutes les modifications ? Une entité regroupant les développeurs (et ceux qui valident les contributions externes) est légitime. Une fondation ou une entreprise par exemple, et par extension un entrepôt centralisé.

Les CI peuvent maintenant signer les artefacts en leur nom (Github par exemple).

Cette confiance peut être chainée, des entrepôts font confiance à la signature de Github.

C’est le principe du Sigstore de Sigstore.

Microsoft, propriétaire de Github, se retrouve quand même avec un beau “All your base are belong to us”.

La gestion de la chaine de confiance dans les entrepôts de paquets (hors paquets des distributions) a longtemps était inexistante ou catastrophique comme l’a démontré la CVE-2022-29176 avec un excellent score de 9.9.

Git

Git n’est pas tout à fait un entrepôt de paquets, mais comme il est possible de désigner une bibliothèque avec une URL Git, on peut le qualifier de paquet source.

Un commit git peut être signé avec GPG, et, depuis peu, avec Sigstore via gitsign.

Python

Python signe les artefatcs de CPython, avec Sgistore et déprécie GPG dans sa PEP-761.

Python galère depuis une dizaine d’années malgré de gros financements (voir la PEP-480 et les discussions attenantes).

Il est possible de signer les paquets avec GPG mais sans trousseau de confiance, ça reste aléatoire.

Plutot de que de mal réinventer la roue, Python choisit l’approche de Sigstore où l’entrepôt, Pypi, signe son index de paquets en s’engageant sur l’identification des publieurs avec l’authentification à deux facteurs (forte) obligatoire depuis fin 2023.

Nodejs

Npm signe les index de son entrepôt avec ECDSA (et abandonne GPG).

Les publieurs peuvent utiliser l’authentification à deux facteurs

Npm ne permet pas de signer un paquet.

Golang

Golang utilise un système non orthodoxe pour la publication de ses modules. Le binaire d’un module n’est publié, uniquement ses sources, accessible par un tas de gestionnaire de source (VCS) : bzr (le machin d’Ubuntu), fossil, git, hg et svn (le CVS des dinos à la page).

Golang propose un proxy, pour ne pas saturer les urls source, un index, pour connaitre les mises à jour disponible et une base de données de checksum (dont les modifications sont auditables) qui contient les hachages des versions des modules, signés par Golang (la clef publique étant connu par la commande go).

Golang fait confiance au service qui héberge le code utilisant des urls de modules non ambigües et universelles.

Docker/OCI

L’entrepôt OCI peu fournir la signature d’une image. L’incontournable Sigstore est utilisé avec l’outil cosign.

Il est possible de téléverser et signer des images OCI sur l’entrepôt open sourcre et éphémère ttls.sh.

PHP

Composer signe ses paquets avec une clef publique (deux en fait).

Un paquet Phar peut être signé avec un hash (une belle confusion technique comme le rappelle la CNIL) ou avec OPENSSL.

Phar.io dit le contraire et vante la signature GPG.

Pour dire vrai, l’écosystème PHP ne m’intéresse plus depuis bien longtemps.

Rust

Rust tergiverse sur l’implémentation de signature de paquet et lorgne vers TUF dans la RFC dédiée et regarde du côté de Sigstore.

Au delà des paquets

Il n’y a pas une technologie universelle de paquets.

L’approche raisonnable est d’empiler les couches :

  • Une distribution Linux utilisant des paquets systèmes (au moins pour sa création).
  • Un conteneur minimaliste (variante slim) qui montera en version plus régulièrement que son hôte.
  • Une application développée avec un nombre raisonnable de paquets spécifiques au langage, périodiquement audités (par un robot comme OSV-scanner) avec des tests pour vérifier la montée en version des dépendances, puis déployée par la CI.

Les langages qui créent de gros blobs (golang, rust, java…) n’ont pas forcément besoin de conteneurs, juste d’isolation (cgroup, namespace…).

Si vous détestez vraiment les paquets, il vous faut de l’unikerl (évoqué dans le billet kernel-less).

Comme dit l’adage :

Les paquets, c’est cadeau

blogroll

social