--- title: "Expérimentation de Fedora CoreOS sur Proxmox : création de la machine virtuelle d'installation" slug: proxmox-fcos2 authors: Florian Maury description: "Un article discutant de la création d'une machine virtuelle permettant l'installation d'autres instances de Fedora CoreOS sur Proxmox et des défis rencontrés" date: 2024-06-03T00:00:00+02:00 type: posts draft: false categories: - devops tags: - proxmox - fcos - systemd lang: fr --- Ce billet est le deuxième d'une série traitant de la création d'une infrastructure virtualisée à l'aide de Proxmox[^proxmox] pour la partie hyperviseur et de Fedora CoreOS[^fcos] pour le système d'exploitation des machines virtuelles invitées (*guests*). L'infrastructure codifiée (*Infrastructure as Code*) est réalisée avec OpenTofu[^opentofu] (Hashicorp Terraform ayant rejoint le côté obscur de la Force). [^proxmox]: https://www.proxmox.com/en/ [^fcos]: https://fedoraproject.org/coreos/ [^opentofu]: https://opentofu.org/ Dans le premier billet[^firstblog], nous avons évoqué les raisons qui ont mené au choix de créer une machine virtuelle d'installation. Dans ce billet, nous allons voir une solution technique preuve de concept satisfaisant ce besoin. Le code source est publié en licence ouverte, afin que la communauté puisse en bénéficier [^sourcecode]. [^firstblog]: https://www.broken-by-design.fr/posts/proxmox-fcos1/ [^sourcecode]: https://git.broken-by-design.fr/fmaury/iac L'objectif est ici d'installer une machine virtuelle (sur Proxmox) qui permettra d'en installer d'autres. La distribution Fedora CoreOS est utilisée pour ses atouts de stabilité et de sécurité intrinsèques (et détaillés dans le premier billet). Sur cette distribution, nous allons déployer un serveur DHCP qui permettra le démarrage par le réseau des futures machines de l'infrastructure. Ce serveur DHCP fournira un script iPXE[^ipxe], une version moderne et en source ouverte de PXE (Preboot eXecution Environment), ainsi que des options DHCP distinctes en fonction de la machine à installer. Ces options permettront notamment de fournir une adresse réticulaire (URL) de type HTTP, afin de récupérer un fichier de configuration Ignition spécifique à chaque machine à installer. Ce fichier Ignition permettra la personnalisation de cette machine à partir de l'image Fedora Core OS générique. Le serveur HTTP est le second service de cette machine virtuelle d'installation. Il permet la publication des configurations Ignition, mais aussi du script iPXE à exécuter, et de l'image de Fedora CoreOS à utiliser pour l'installation. Le dernier service de cette machine d'installation est un serveur de fichiers sur lequel Opentofu (ou Terraform si vous y tenez vraiment) pourra déposer des extensions de configuration DHCP et des fichiers Ignition : un pour chaque machine virtuelle de l'infrastructure à installer. Dans cette preuve de concept, le serveur de fichiers est un serveur SFTP. Nous verrons dans la suite de cet article que c'est un choix par défaut, et qui n'est pas entièrement satisfaisant. [^ipxe]: https://ipxe.org/ ## Déploiement de services sur Fedora CoreOS ### Extensions par conteneurs Fedora CoreOS est une distribution optimisée pour l'hébergement de conteneurs. Son socle (constitué par les namespaces[^ns] par défaut) ne contient que très peu de programmes, et les interactions entre les différents logiciels le composant ainsi qu'avec les conteneurs sont fortement limitées par le durcissement mis en place, notamment avec SELinux. [^ns]: https://www.man7.org/linux/man-pages/man7/namespaces.7.html Cette distribution permet bien d'installer des logiciels additionnels sur le socle, grâce à un système de surcouches (*layering*) de rpm-ostree[^rpm-ostree]. Cette éventualité doit cependant être écartées dans le cas de cette machine virtuelle d'installation ; en effet, celle-ci démarre sur un LiveCD ISO et la propriété d'immuabilité de Fedora CoreOS "interdit" l'ajout de nouveaux programmes sur le système en cours d'exécution. [^rpm-ostree]: https://coreos.github.io/rpm-ostree/ L'ensemble des services déployés doivent donc l'être grâce à des conteneurs. Ce n'est pas si différent de ce qui doit être fait dans un cluster Kubernetes, et il n'est pas étonnant que Red Hat CoreOS soit utilisée comme socle pour la plateforme OpenShift[^openshift]. [^openshift]: https://www.redhat.com/fr/technologies/cloud-computing/openshift De même, les volumes des conteneurs ne sont qu'assez rarement des bind-mount[^bindmounts] de répertoires arbitraires du socle, et plus généralement des volumes (anonymes ou nommés) du moteur de conteneurs. Cela est dû aux politiques SELinux[^container_selinux] qui restreignent les interactions avec les fichiers dans les volumes, qui doivent avoir (principalement) les types `container_file_t` ou `container_ro_file_t`[^container_te], ce qu'ont rarement les fichiers du socle. [^bindmounts]: https://docs.docker.com/storage/bind-mounts/ [^container_selinux]: https://github.com/containers/container-selinux/blob/main/container_selinux.8 [^container_te]: https://github.com/containers/container-selinux/blob/main/container.te Ainsi, le service DHCP et le service HTTP sont déployés sous la forme de conteneurs, et en conséquence, le serveur de fichiers doit l'être également, puisqu'il sert à ajouter du contenu aux volumes exposés aux deux premiers services. Une autre conséquence est que si un conteneur doit disposer de fichiers pré-existants dans des volumes, il faut trouver un moyen de les y placer. La méthode la plus propre et la plus simple est d'utiliser des conteneurs d'initialisation[^init_containers], à l'instar de ce qui est fait dans les déploiements Kubernetes. Podman dispose également de la commande `podman kube` qui permet d'émuler en partie les déploiements Kubernetes et notamment les objets ConfigMap[^config_map]. Les ConfigMaps étant plus "limitées" que le dépôts de fichiers arbitraires, il a été choisi pour cette preuve de concept d'utiliser exclusivement des conteneurs d'initialisation. [^init_containers]: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ [^config_map]: https://kubernetes.io/docs/concepts/configuration/configmap/ ### Les Podman Quadlets Fedora CoreOS dispose par défaut des moteurs Moby (Docker) et Podman. S'il est bien sûr possible d'utiliser `compose` pour définir les conteneurs, les orchestrer et les lancer, il est également possible d'utiliser les Podman Quadlets[^quadlets]. Les Quadlets sont des fichiers de configuration ressemblant à des unités systemd (*systemd units*), avec des sections en plus. Il en existe de plusieurs types : conteneurs, pods, images, volumes, réseaux, et même une couche de compatibilité avec la syntaxe Kubernetes. [^quadlets]: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html Voici par exemple le quadlet pour le volume stockant les baux DHCP de notre preuve de concept, stocké dans le fichier /etc/containers/systemd/dhcp_data.volume : ``` [Unit] Description = DHCP Data Volume [Volume] VolumeName = dhcp_data Device=/dev/disk/by-label/dhcp_data Options=nodev,noexec,nosuid,rootcontext=system_u:object_r:container_file_t:s0 Type=ext4 ``` Le quadlet du conteneur d'initialisation téléchargeant la dernière image de Fedora CoreOS est : ``` [Unit] Description = Download Latest FCOS Image [Container] ContainerName = fcos_downloader Image = image_downloader.image Exec = download -s stable -f pxe Volume = fcos_images.volume:/data:z WorkingDir = /data [Install] WantedBy=multi-user.target ``` Ils sont consommés par un générateur de services systemd qui transforme ces quadlets, situés dans `/etc/containers/systemd`, en services, situés dans `/run/systemd/generator`. L'un des intérêts des quadlets est leur intégration au sein de systemd, avec la possibilité de contrôler de manière assez fine les dépendances avec d'autres services. En outre, leur syntaxe permet également de monter automatiquement des systèmes de fichiers sur des volumes. Couplé à la facilité de créer des partitions avec Ignition, il est ainsi trivial de créer une partition par volume. Il s'agit d'une bonne pratique de sécurité qui permet de limiter les risques de dénis de service ou de corruption entre conteneurs par saturation d'un système de fichiers commun à plusieurs volumes. ## Difficultés rencontrées Un projet ne serait pas une aventure, sans son lot d'ornières et d'imprévus. Quelle aventure se fut ! ### Problèmes avec le fournisseur Ignition d'Opentofu Les fichiers de configuration Ignition sont des documents JSON. Chaque document JSON décrit une machine complète : partitions, systèmes de fichiers, fichiers de configuration systemd, fichiers arbitaires, répertoires, utilisateurs et groupes, etc. Le contenu des fichiers à créer est sérialisé dans ce document JSON, soit verbatim, soit sous la forme d'une adresse de type `data:`[^rfc_data_url]. [^rfc_data_url]: https://www.rfc-editor.org/rfc/rfc2397 Il est possible qu'un fichier de configuration Ignition contienne des directives d'inclusion/fusion d'autres fichiers Ignition pouvant être récupérés notamment par le réseau. Il faut dans ce cas s'assurer de l'intégrité de ces fichiers distants, afin de prévenir la compromission totale du serveur. Fedora CoreOS étant un système d'exploitation immuable, reposant sur des images systèmes administrées par `rpm-ostree`, l'image de base est la même pour toutes les machines d'une infrastructure virtualisée ; les machines se diversifient et se spécialisent via leur configuration Ignition. Cette approche est donc opposée à celles impliquant la pré-personnalisation des images avec des outils comme Packer[^packer]. [^packer]: https://www.packer.io/ Le moment et la manière de générer la configuration Ignition peut varier en fonction des préférences individuelles. Le plus gros de la configuration peut être statique : tous les serveurs HTTP ont besoin des mêmes images de conteneurs, des mêmes scripts de démarrage, des mêmes partitions et volumes pour stocker certaines informations de manière persistante. Tout cela peut être stocké dans un fichier Ignition commun à toutes les instances et ultérieurement inclus, ou être mis dans le fichier Ignition de chaque instance. C'est au choix. Avec une approche cloud-init, ces fichiers feraient parties de l'image générée avec Packer. En revanche, le contenu servi par ces serveurs HTTP ou l'adresse IP d'écoute sont des informations spécifiques à chaque instance. Avec l'approche cloud-init, ces informations seraient communiquées au système d'exploitation par le service de métadonnées interrogé par l'utilitaire cloud-init. Dans le cas d'espèce, puisque nous n'avons pas encore d'infrastructure virtualisée, pas de serveur HTTP de confiance où stocker et distribuer la configuration commune à toutes les machines virtuelles d'installation, et que le fichier Ignition va être stocké dans un ISO personnalisé de Fedora CoreOS, nous n'allons générer qu'un seul fichier Ignition, contenant les informations communes à plusieurs instances éventuelles et les informations spécifiques à une instance spécifique. Concernant la conception du document JSON au format Ignition, nous pourrions écrire cette configuration Ignition avec n'importe quel outil, y compris "manuellement", mais ultérieurement, nous le ferons avec Opentofu, au moins pour les informations spécifiques. Par cohérence, Opentofu a donc également été utilisé pour cette machine. Étant donné que les fichiers Ignition sont exprimés en JSON, il est possible de structurer ses données en HCL (HashiCorp Configuration Language), puis de faire appel à la fonction `jsonencode`. Pour autant, Hashicorp a initialement développé un fournisseur Terraform pour Ignition[^hashicorp_ignition], qui a ensuite été abandonné, puis repris par la communauté[^community_ignition]. Ce fournisseur propose des sources de données (*data sources*) afin de structurer la configuration Terraform et la typer. [^hashicorp_ignition]: https://github.com/hashicorp/terraform-provider-ignition [^community_ignition]: https://github.com/community-terraform-providers/terraform-provider-ignition Ce fournisseur est assez basique puisqu'il se contente de codifier sous la forme d'un schéma de source de données les champs des différentes structures définies dans la spécification d'Ignition[^ignition_spec]. [^ignition_spec]: https://coreos.github.io/ignition/configuration-v3_4/ Avec le fournisseur, on écrit donc : ```hcl data "ignition_file" "dnsmasq_container" { path = "/etc/containers/systemd/dnsmasq.container" mode = 420 content { content = file("${path.module}/files/dnsmasq.container") } } data "ignition_config" "example" { files = [ data.ignition_file.dnsmasq_container.rendered, ] } locals { serialized_config = data.ignition_config.example.rendered } ``` En HCL "pur", pour comparaison, on écrit : ```hcl locals { dnsmasq_container_file = { path = "/etc/containers/systemd/dnsmasq.container" mode = 420 contents = { source = format( "data:text/plain;base64,%s", base64encode(file("${path.module}/files/dnsmasq.container")) ) } } serialized_config = jsonencode({ ignition = { version = "3.4.0" } storage = { files = [ local.dnsmasq_container_file, ] } }) } ``` La différence de verbosité entre les deux versions n'est pas flagrante ; mais surtout, le fournisseur comporte plusieurs problèmes : le document JSON généré n'est pas minimal, certaines générations sont incorrectes, et il manque des options pourtant spécifiées dans la version 3.4.0 du format Ignition. La génération non-minimaliste est dû à l'usage par le fournisseur des types définis dans le code source de l'outil Ignition lui-même[^incorrecttypes]. Par exemple la structure racine d'une configuration Ignition est définie ainsi : [^incorrecttypes]: https://github.com/coreos/ignition/blob/v2.18.0/config/v3_4/types/schema.go ``` type Ignition struct { Config IgnitionConfig `json:"config,omitempty"` Proxy Proxy `json:"proxy,omitempty"` Security Security `json:"security,omitempty"` Timeouts Timeouts `json:"timeouts,omitempty"` Version string `json:"version"` } ``` La plupart des champs sont définis avec l'annotation de sérialisation JSON `omitempty`. Quand cette annotation est utilisée de manière correcte, le champ n'apparait pas dans le document JSON généré avec la fonction `json.Marshal` si sa valeur est "fausse" ou "vide" selon le système de typage du langage Go. Or, les sous-structures ne sont pas définies comme des pointeurs. Ainsi, elles ne peuvent jamais être "vides", et les champs sont toujours ajoutés au document JSON, même si elles ne contiennent aucune valeur. Voici la configuration générée par le code ci-dessus utilisant le fournisseur : ``` { "ignition": { "config": { "replace": { "verification": {} } }, "proxy": {}, "security": { "tls": {} }, "timeouts": {}, "version": "3.4.0" }, "kernelArguments": {}, "passwd": {}, "storage": { "files": [ { "group": {}, "overwrite": false, "path": "/etc/containers/systemd/dnsmasq.container", "user": {}, "contents": { "source": "data:text/plain;charset=utf-8;base64,W1VuaXRdCkRlc2NyaXB0aW9uID0gREhDUCBDb250YWluZXIKCldhbnRzPWltYWdlX2Rvd25sb2FkZXIuc2VydmljZQpBZnRlcj1pbWFnZV9kb3dubG9hZGVyLnNlcnZpY2UKV2FudHM9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW5ldHdvcmstb25saW5lLnRhcmdldApXYW50cz1kaGNwX2NvbmZpZ19pbml0LnNlcnZpY2UKQWZ0ZXI9ZGhjcF9jb25maWdfaW5pdC5zZXJ2aWNlCgpbQ29udGFpbmVyXQpDb250YWluZXJOYW1lID0gZG5zbWFzcV9jb250YWluZXIKSW1hZ2UgPSBsb2NhbGhvc3QvZG5zbWFzcTpsYXRlc3QKVm9sdW1lID0gZGhjcF9jb25maWcudm9sdW1lOi9ldGMvZG5zbWFzcS5kOnoKVm9sdW1lID0gZGhjcF9kYXRhLnZvbHVtZTovZGF0YTpaClZvbHVtZSA9IC9kZXYvbG9nOi9kZXYvbG9nCk5ldHdvcmsgPSBob3N0CkFkZENhcGFiaWxpdHkgPSBDQVBfTkVUX0FETUlOLENBUF9ORVRfUkFXCgpbU2VydmljZV0KV29ya2luZ0RpcmVjdG9yeT0vdmFyL3Jvb3Rob21lL2RoY3AKRXhlY1N0YXJ0UHJlPS9iaW4vYmFzaCAvdmFyL3Jvb3Rob21lL2dlbmVyYXRlX2RoY3Bfb3B0aW9ucy5zaApFeGVjU3RhcnRQcmU9L3Vzci9iaW4vcG9kbWFuIGJ1aWxkIC10IGRuc21hc3E6bGF0ZXN0IC4KUmVzdGFydD1vbi1mYWlsdXJlCgpbSW5zdGFsbF0KV2FudGVkQnk9bXVsdGktdXNlci50YXJnZXQKCg==", "verification": {} }, "mode": 420 } ] }, "systemd": {} } ``` En comparaison, voici le document JSON généré en écrivant soi-même en HCL la configuration Ignition : ``` { "ignition": { "version": "3.4.0" }, "storage": { "files": [ { "contents": { "source": "data:text/plain;base64,W1VuaXRdCkRlc2NyaXB0aW9uID0gREhDUCBDb250YWluZXIKCldhbnRzPWltYWdlX2Rvd25sb2FkZXIuc2VydmljZQpBZnRlcj1pbWFnZV9kb3dubG9hZGVyLnNlcnZpY2UKV2FudHM9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW5ldHdvcmstb25saW5lLnRhcmdldApXYW50cz1kaGNwX2NvbmZpZ19pbml0LnNlcnZpY2UKQWZ0ZXI9ZGhjcF9jb25maWdfaW5pdC5zZXJ2aWNlCgpbQ29udGFpbmVyXQpDb250YWluZXJOYW1lID0gZG5zbWFzcV9jb250YWluZXIKSW1hZ2UgPSBsb2NhbGhvc3QvZG5zbWFzcTpsYXRlc3QKVm9sdW1lID0gZGhjcF9jb25maWcudm9sdW1lOi9ldGMvZG5zbWFzcS5kOnoKVm9sdW1lID0gZGhjcF9kYXRhLnZvbHVtZTovZGF0YTpaClZvbHVtZSA9IC9kZXYvbG9nOi9kZXYvbG9nCk5ldHdvcmsgPSBob3N0CkFkZENhcGFiaWxpdHkgPSBDQVBfTkVUX0FETUlOLENBUF9ORVRfUkFXCgpbU2VydmljZV0KV29ya2luZ0RpcmVjdG9yeT0vdmFyL3Jvb3Rob21lL2RoY3AKRXhlY1N0YXJ0UHJlPS9iaW4vYmFzaCAvdmFyL3Jvb3Rob21lL2dlbmVyYXRlX2RoY3Bfb3B0aW9ucy5zaApFeGVjU3RhcnRQcmU9L3Vzci9iaW4vcG9kbWFuIGJ1aWxkIC10IGRuc21hc3E6bGF0ZXN0IC4KUmVzdGFydD1vbi1mYWlsdXJlCgpbSW5zdGFsbF0KV2FudGVkQnk9bXVsdGktdXNlci50YXJnZXQKCg==" }, "mode": 420, "path": "/etc/containers/systemd/dnsmasq.container" } ] } } ``` Cette définition incorrecte des types dans le code source d'Ignition n'est pas trivial à corriger car ces types sont générés par un outil appelé `schematyper`[^schematyper]. Cet outil prend en entrée une description de données au format JSON Schema[^json_schema], et écrit en sortie des définitions de structures en Go. [^json_schema]: https://json-schema.org/ [^schematyper]: https://github.com/idubinskiy/schematyper Dans le cas d'espèce, cette verbosité excessive n'est pas forcément gênante car nous n'avons pas de contrainte de taille de fichiers. Certains services de metadonnées cloud en ont cependant une (généralement autour de 16ko) ; dans ces cas, la taille compte. Également, bien que les champs soient bien définis par le code source d'Ignition, le fournisseur Terraform est incomplet et plusieurs définitions manquent, rendant impossible l'utilisation de certaines options. Ce n'est pas que c'est difficile à ajouter... mais leur absence cumulée au fait qu'exprimer en HCL une configuration Ignition fait qu'il devient immédiatement préférable de s'en passer. ### Problèmes avec la prise en charge de SFTP par Opentofu La solution proposée utilise SFTP afin de permettre l'extension de la configuration du serveur DHCP et la publication de fichiers Ignition pour les futures machines virtuelles qui composeront l'infrastructure. SFTP est un service natif d'OpenSSH, un des démons les plus exposés et les plus sensibles puisque sa présence est quasi universelle et qu'il transporte notamment les flux d'administration. Son emploi est particulièrement intéressant pour le type de transfert de fichiers utilisé dans cette preuve de concept, grâce à son chiffrement de flux par défaut, son identification native par clé publique, et ses capacités d'isolation à l'aide de chroot[^chroot]. [^chroot]: `chroot(2)` est un appel système qui permet de restreindre la capacité d'un processus à changer de répertoires ; bien utilisé, il permet de restreindre les accès à un sous-ensemble de la hiérarchie des fichiers d'un système de fichiers sous Linux. De surcroit, la configuration est triviale : ``` Subsystem sftp internal-sftp Match User terraform_ignition ForceCommand internal-sftp ChrootDirectory /my/chroot/path ``` Compte tenu des politiques de sécurité SELinux en place par défaut sur Fedora CoreOS, il n'est pas possible d'utiliser le processus OpenSSH Server du socle. En effet, lors de la réception des fichiers, ces derniers sont marqués avec le type SELinux `user_home_t` avec lequel les conteneurs ne peuvent interagir. Pour cette raison, la preuve de concept dispose de deux serveurs SSH : un pour se connecter au socle et un autre, accessible uniquement en SFTP, permettant le téléversement de fichiers. Ainsi, les fichiers déposés en SFTP sont marqués avec le type `container_file_t` qui peut être lu par d'autres conteneurs. Hélas, ce superbe service SFTP ne peut être utilisé nativement par Opentofu... En effet, l'espoir était que les configurations DHCP soient générées par Opentofu, écrites sur disque à l'aide d'une ressource `local_file`[^local_file_res] ou même une `null_resource`[^null_res], puis téléversées grâce au mécanisme d'approvisionnement (*provisioner*) "file"[^file_prov]. Le code aurait ressemblé à : [^local_file_res]: https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file [^null_res]: https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource [^file_prov]: https://developer.hashicorp.com/terraform/language/resources/provisioners/file ```hcl resource "null_resource" "ignition_configuration" { provisioner "file" { content = local.encoded_config destination = "writable/${vm_id}.ign" connection { type = "ssh" host = var.netboot_server_ip port = 2222 user = "terraform_ignition" agent = true bastion_host = var.pve_host bastion_user = var.pve_pam_user bastion_port = 22 } } } ``` Il s'avère néanmoins que cela n'est pas possible, et une tentative renvoie le message d'erreur : ``` Upload failed: his service allows sftp connections only` ``` La documentation explique que : > Provisioners which execute commands on a remote system via a protocol such as > SSH typically achieve that by uploading a script file to the remote system and > then asking the default shell to execute it. Ce mécanisme d'approvisionnement ayant pour but la copie de fichiers notamment par SSH n'est pas compatible avec le mécanisme standard de transfert de fichiers de SSH[^scp]... [^scp]: Le protocole SCP est devenu obsolète à la suite d'une vulnérabilité protocolaire irréparable : https://lwn.net/Articles/835962/ La preuve de concept actuelle repose donc sur plusieurs mécanismes d'approvisionnement, dont `local-exec`[^local_exec] afin d'exécuter la commande `sftp` via le shell. Comme cette solution est un bricolage peu satisfaisant, l'auteur de cet article envisage de développer, dans un futur plus ou moins proche, un fournisseur Opentofu pour la gestion de ressources de type fichiers au travers du protocole WebDav[^webdav], afin de palier cette situation. [^local_exec]: https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec [^webdav]: https://www.rfc-editor.org/rfc/rfc4918 ### Problèmes avec le fournisseur Opentofu pour Proxmox Le fournisseur Opentofu pour Proxmox `bpg/proxmox`[^bpg] est le fournisseur le plus avancé disponible sur les registres de Terraform. [^bpg]: https://registry.terraform.io/providers/bpg/proxmox/latest/docs Hélas, ce dernier ne dispose pas d'une ressource pour le téléversement de fichiers ISO. La ressource `proxmox_virtual_environement_file` permet certes le téléversement de fichiers arbitraires, mais comme l'indique la documentation, un transfert par SSH vers l'hyperviseur est impliqué, au lieu d'utiliser l'API de Proxmox. La surface d'attaque de cette ressource est donc bien plus importante que si l'API avait été utilisée. C'est d'autant plus regrettable que l'API fournit bien un moyen de téléverser des fichiers ISO. En conséquence, dans cette preuve de concept, le mécanisme d'approvisionnement `local-exec` d'Opentofu a été utilisé afin de téléverser le fichier avec l'utilitaire `curl` par l'API de Proxmox : ``` provisioner "local-exec" { command = <