Immuabilité, PHP et anti-patterns
Cet article est écrit à la suite d’une tentative d’installation infructueuse de la suite NextCloud, au travers de son image OCI (“Docker”) sur Fedora Core OS. L’analyse post-mortem de cet échec, ses raisons et de l’écosystème a permis de mettre le doigt sur un anti-pattern (c’est-à-dire une conception commune aux effets délétères). Celui-ci est particulièrement observable sur les applications développées en PHP, et consiste en la violation du principe d’immuabilité. Explications.
L’immuabilité : qu’est-ce que c’est ?
L’administration système à l’ancienne
Il y a 20 ans, les administrateurs et administratrices système se connectaient directement aux serveurs à administrer, et opéraient les changements à la main. Les plus téméraires pouvaient éventuellement modifier plusieurs serveurs en même temps avec des solutions ad hoc, comme Parallel SSH. C’est le genre d’histoires que se racontent au coin du feu les jeunes adminsys, les soirs de nouvelle lune.
Les outils de gestion de la configuration
Des outils de gestion de la configuration, comme Puppet, Chef, cfengine, ou encore Ansible sont ensuite progressivement apparus. Ces outils permettent de dresser une liste de changements de configuration à opérer sur un système ou un ensemble de systèmes. Cette liste peut alors être enregistrée dans un logiciel de gestion de versions, comme Git.
Ces outils permettent une certaine traçabilité des changements de configuration, ce qui constitue un gain significatif en comparaison des modifications manuelles. Cependant, ces outils n’offrent pas une vision parfaite de l’état des systèmes administrés. En effet, ils font l’hypothèse que le serveur est dans un état X, et ils tentent de l’amener vers un état X+1. Or, de nombreux imprévus sont possibles. Par exemple, le serveur peut avoir été altéré manuellement (“snowflake servers”). Ou alors, l’hypothèse de départ X est fausse (i.e. le serveur est dans un état Y). Ou encore, l’application des changements échoue en plein milieu, laissant un serveur dans un état X+0.61, tandis que celui d’à côté est dans l’état X+0.23 en fonction de quand l’erreur est survenue pour chacun de ces serveurs. Ces dérives de configuration entre les états souhaités, supposés et réels sont nommées “configuration drifts” en anglais.
L’immuabilité
Les approches manuelles et par l’entremise d’outils de gestion de la configuration sont qualifiées de “mutable”, en anglais. En français, le mot “mutable” est un emprunt, rarement référencé dans les dictionnaires. Son antonyme est “immuable”, traduction de “immutable”. Le Larousse propose les antonymes suivants pour immuable : caduc, fugace, modifiable, momentané, passager, précaire. Ces mots n’inspirent guère ce qu’il semblerait souhaitable pour un système.
Afin de remédier à cette situation, d’autres approches, dites immuables, ont vu le jour. Il est, par exemple, possible de citer Terraform, Fedora Core OS ou les images OCI (“Docker”).
Terraform/OpenTofu
L’idée générale de Terrform/OpenTofu est d’utiliser une syntaxe descriptive des ressources composant un système d’information. Ces ressources peuvent être de divers ordres, mais l’on compte parmi ces dernières des machines.
Chacune de ces ressources est spécifiée à l’aide d’attributs. Lorsque certains attributs sont modifiés, la ressource peut être altérée (“mutée”), ou détruite et recrée. C’est uniquement dans ce second cas que l’on peut à proprement parler d’immuabilité.
L’immuabilité intervient parce qu’une ressource est créée dans un état, et la seule manière de la modifier est de la détruire et d’en créer une nouvelle, à jour. Cette approche prévient les dérives de configuration, du fait qu’il n’existe pas de modification du système. Le système est donc dans l’état supposé, testé, et validé, dans le cas nominal. En cas de compromission, il est aisé de comparer l’état réel par rapport à l’état supposé, celui-ci ayant été déclaré dans la configuration Terraform, et son rétablissement s’effectue par la destruction de la ressource compromise.
Terraform/OpenTofu n’est cependant pas vraiment adapté à la configuration fine du système installé. En effet, ces derniers sont du ressort d’un composant appelé système d’approvisionnement (“provisionner”). Bien qu’il existe une multitude de systèmes d’approvisionnement, y compris des outils de gestion de la configuration, certains sont plus adaptés que d’autres à l’approche immuable. Parmi les plus répandus, il est possible de citer cloud-init et ignition. Ces outils sont notamment capables de consommer les user-data, un attribut qui peut être défini par Terraform/OpenTofu lors de la création d’une machine virtuelle.
Fedora Core OS
Fedora Core OS est une distribution Linux immuable, durcie, minimaliste et orientée vers l’hébergement de conteneurs, reposant sur les paquets de la distribution Fedora.
L’immuabilité de cette distribution Linux est accomplie d’une part par l’entremise de rpm-ostree et de sa capacité à tourner exclusivement en RAM. rpm-ostree est un outil de versionnement des systèmes de fichiers. Il permet notamment de déployer, ou redéployer, une version cohérente, connue, testée et approuvée de la distribution Linux. Cette approche permet d’éliminer tout changement éphémère qui aurait transformé le serveur en “snowflake”. En outre, son outil de provisionnement, ignition, exécuté lors du premier démarrage d’un serveur Fedora Core OS, permet de consommer les user-data (entre autres sources de données), afin de configurer la machine, une fois pour toutes.
Lorsque des fichiers doivent être stockés pour une durée de vie supérieure à une installation de Fedora Core OS, ces derniers peuvent être stockés sur une ou plusieurs partitions dédiées, ou sur un partage réseau, par exemple via NFS.
Images OCI
De leur côté, les images OCI (“Docker”) sont créées et distribuées sous la forme de couches immuables, c’est-à-dire en lecture seule. Chacune de ces couches est générée en observant les modifications apportées au système de fichiers (ou aux métadonnées de l’image) par l’exécution d’une directive, pendant l’étape de construction de l’image (“build”).
Afin de pouvoir rendre un service, ces couches sont assemblées, superposées, puis exécutées sous la forme de conteneurs. Les conteneurs ainsi exécutés peuvent être eux-mêmes en lecture seule, assurant ainsi que le code exécuté est exactement celui qui a été spécifié dans l’image. Ils peuvent être également en lecture-écriture, mais ces changements sont éphémères et perdus à l’extinction du conteneur.
Il est bien entendu possible de faire perdurer des données au-delà de l’extinction du conteneur, à l’aide de volumes. Les volumes sont des fichiers ou des répertoires pouvant être attachés à des chemins particuliers sur le système de fichiers des conteneurs, et dont la durée de vie est supérieure à celle des conteneurs auxquels ils sont attachés.
Infrastructure immuable
En assemblant ces composants entre eux, il est possible de créer une machine virtuelle avec Terraform/OpenTofu, faisant tourner Fedora Core OS. Lors du premier démarrage, ignition consomme les user-data définies par Terraform/OpenTofu, afin de générer la configuration système. Cette configuration contient les disques réseau pour la persistance des volumes podman ou docker, et la liste des images OCI à télécharger et des conteneurs à créer.
Lorsqu’une mise à jour de la configuration est nécessaire, la machine est détruite et une nouvelle machine virtuelle, avec des nouvelles user-data, est créée pour la remplacer.
Cette approche permet à tout moment d’avoir une vision claire des services déployés, et de leur configuration. L’audit, ou le redéploiement après incident sont simplifiés et toute dérive de la configuration est naturellement découragée par le caractère éphémère des déploiements.
PHP et imports dynamiques
Particularités du PHP
PHP est un langage de programmation populaire, ayant une base bien établie. Aisé d’accès, dynamique, il permet une grande expressivité, et est équipé de nombreux outils et sucres syntaxiques.
Parmi ses atouts, on peut citer une syntaxe relativement lisible, qui permet d’exprimer directement en PHP des configurations facilement éditables, y compris par des néophytes en développement logiciel. Il peut également être cité l’autochargement de classes, par inférence de leur chemin d’import.
Exécution de code arbitraire par inclusion locale
Hélas, les atouts de PHP sont à double tranchant, et ils sont la source de vulnérabilités dont l’existence est quasi exclusive à ce langage. C’est notamment le cas des LFI (local file inclusions), qui sont un type de vulnérabilités dont l’exploitation peut consister à inciter un code PHP vulnérable à importer et exécuter un fichier local contenant le code d’un attaquant. Ce fichier contrôlé par l’attaquant est généralement déposé par ses soins, en abusant de sa capacité à écrire sur le système de fichiers du serveur PHP, que ce soit dans les journaux applicatifs, ou par les fonctionnalités de téléversements de fichiers (images polyglottes, défaut de contrôle, mécanismes d’extension ("plugins"), etc.).
Les exécutions de code arbitraire par le biais de LFI sont des exploitations de vulnérabilités rendues possibles par le non-respect du principe de sécurité dénommé W^X ou Write XOR eXecute. Ce principe indique qu’une ressource est soit inscriptible, soit exécutable, mais jamais les deux, ni l’un puis l’autre, et ce pendant l’intégralité de la durée de vie de cette ressource. Ce principe est souvent complexe à respecter dans le cas des langages interprétés, comme PHP, puisque les fichiers de code source ne sont pas exécutables, mais constituent néanmoins la source du contrôle de l’exécution. PHP a disposé, il y a fort longtemps, d’un patch de sécurité nommé Suhosin.
Parmi ses nombreuses fonctionnalités de sécurité, ce patch modifiait l’interpréteur pour ne l’autoriser qu’à exécuter le code PHP contenu dans des fichiers en lecture seule. Hélas, ce patch n’est plus maintenu. Le respect de ce principe est donc entièrement dans les mains des développeurs qui doivent s’abstenir d’inclure des fichiers provenant de fichiers ou de chemins inscriptibles. Cela implique de ne pas permettre à une application de modifier son propre code, que ce soit pour se mettre à jour, s’étendre, ou se configurer.
Il convient de noter que modifier son propre code, outre les risques d’intégrité évoqués dans cette section, est également risqué d’un point de vue disponibilité, puisqu’il contrevient à l’immuabilité, principe évoqué plus haut dans cet article.
Conteneurs PHP
Les applications PHP n’ont pas échappé au tsunami de la conteneurisation. C’est notamment le cas des moteurs Drupal et Wordpress, mais aussi d’applicatifs complexes comme NextCloud.
Les conteneurs suivent, par essence, les préceptes de l’immuabilité, chaque couche étant en lecture seule, à l’exception de la dernière couche. Cette dernière peut être inscriptible, mais sans persistance des données en cas d’arrêt du conteneur. Cet environnement semble ainsi antagoniste aux aspirations des développeurs PHP qui souhaitent que leur code soit modifiable.
Afin de contourner ce qui est certainement jugé comme une limitation et une inconvenance par ces développeurs, ces derniers ont alors résolu l’incompatibilité de leurs applicatifs avec les conteneurs en stockant leur code exécutable dans des volumes ! Les volumes étant inscriptibles et persistants, ils permettent aux applicatifs PHP de retrouver la liberté de modifier leur code. Ainsi, à la date d’écriture de ces lignes, les images OCI des applicatifs NextCloud, Drupal et Wordpress recopient par rsync (ou assimilé) le contenu d’un répertoire contenu dans l’image OCI dans /var/www/html où est censé se trouver un volume. Ils peuvent alors faire persister entre les redémarrages de conteneurs les modifications de code.
La pratique de recopier le code applicatif dans un volume, afin de faire persister des modifications de code, contrevient totalement aux objectifs de l’immuabilité, puisqu’il est alors possible d’avoir un conteneur faisant tourner un interpréteur PHP en version X, pour un code applicatif modifié ultérieurement en version Y ou Z. Les capacités de tests, d’audit et de conformité des installations que sont censés apporter les conteneurs sont alors totalement annulées.
En outre, dans le cas de Nextcloud, l’utilisation de rsync pour la recopie du code de l’application avec l’option archive implique des changements de propriétaire pour certains fichiers. Or, ce changement de propriétaire est incompatible avec certaines technologies utilisées de manière fréquente pour le stockage des volumes. C’est le cas notamment du système de fichiers réseau NFS.
Il est à souligner que la pratique a été sujette à débat dans la communauté Drupal ; pour cette raison, le conteneur Drupal n’indique pas explicitement qu’un volume est attendu à l’emplacement /var/www/html. Néanmoins, ne pas en monter un à cet endroit peut résulter en des dysfonctionnements majeurs, puisque des données utilisateurs ou de configuration peuvent être perdues lors de l’arrêt du conteneur. Wordpress indique dans sa documentation comment déployer une version immuable de son code ; ce n’est cependant pas le mode par défaut. NextCloud ne prévoit aucun mécanisme permettant de rendre immuable son conteneur.
Des conteneurs PHP immuables
Les développeurs de ces applicatifs PHP avaient-iels le choix ? Existe-t-il des moyens de développer des applications PHP extensibles, configurables et pouvant être mises à jour tout en respectant le principe d’immuabilité ?
La réponse est bien entendu oui.
Les fichiers de configuration
Tout d’abord, la pratique commune de placer la configuration applicative dans des fichiers .php situés dans le même répertoire que le code source devrait être évitée à tout prix. Placer cette configuration dans un répertoire dédié permettrait de monter un volume à cet emplacement, de manière à faire persister la configuration entre les redémarrages, sans que l’ensemble des fichiers de l’application se retrouvent dans le volume.
Il existe bien sûr la possibilité d’utiliser des volumes anonymes ou la directive subpath K8S, afin d’effectuer des bind-mount sur un unique fichier. Cette pratique est cependant peu recommandée, car elle fait l’hypothèse que le fichier source du bind-mount ne changera pas d’inode pendant toute la durée de vie du conteneur. Cette hypothèse est aisément cassée, ne serait-ce que par inadvertance.
Les mises à jour
Automodifier son code peut être perçu par les développeurs PHP comme une bonne pratique de sécurité, afin de limiter le risque que des installations soient compromises par manque de mise à jour.
Il s’agit néanmoins d’une mauvaise pratique qui tire ses racines dans un manque de connaissance ou de reconnaissance des pratiques opérationnelles et de déploiement des applicatifs conteneurisés. En effet, la mise à jour de conteneur peut être automatisée à l’aide d’outils comme WatchTower, pour Docker. Dans le cadre de Podman, podman-auto-update sert un rôle similaire.
La pertinence de la mise à jour automatique reste cependant une question débattue. En effet, automatiser la mise à jour sans automatiser également les tests de qualité avant la mise en production semble présenter un risque pour la disponibilité du service. Une chose reste sûre : laisser la mise à jour (partielle) entre les mains des utilisateurs du service semble encore plus hasardeux.
Mécanismes d’extension
Les images OCI sont par nature extensibles. En effet, chaque couche constituant l’image est une extension, venant à ajouter ou supprimer du contenu à l’image finale.
Ainsi, pour créer une application extensible et distribuée sous la forme d’images OCI, le seul prérequis est que l’administrateur système produise son propre artéfact et le déploie au lieu de tirer ("pull") une image d’un registre public (hub.docker.com, quay.io, etc.). Cet artéfact peut alors intégrer, par exemple avec la directory COPY des Dockerfile/Containerfile, n’importe quel code additionnel venant enrichir l’application. L’avantage de cette approche est qu’une certaine flexibilité est gardée, tout en permettant d’adopter l’approche immuable, plus stable, auditable et testable.
Conclusion
Dans cet article, nous avons étudié comment les images OCI de certaines applications PHP populaires sont conçues d’une manière qui contrevient aux principes d’immuabilité, principes au coeur du concept même des conteneurs.
Ainsi construites, ces images sont tout à la fois moins fiables, moins stables, moins sécurisées, moins auditables, moins testables — en résumé, moins qualitatives — que les alternatives immuables qui auraient pu être construites à la place.
Nous avons également énuméré plusieurs cas d’usage pouvant avoir été à l’origine de ces choix de conception (mise à jour, extensibilité, configurabilité). Nous les avons ensuite décortiqués un à un, en prouvant que des approches immuables peuvent apporter une flexibilité comparable et une sécurité supérieure.
Eu égard à ces conclusions, les communautés de ces outils sont encouragées à adopter une approche immuable, plus proche des principes fondateurs des clouds modernes et de l’état de l’art en général.