From 58607f70e448bb86e66fda986143c9c385fc7be0 Mon Sep 17 00:00:00 2001 From: Florian Maury Date: Tue, 4 Jun 2024 17:33:29 +0200 Subject: [PATCH] Add second post on FCOS on proxmox --- posts/proxmox-fcos2.md | 711 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 711 insertions(+) create mode 100644 posts/proxmox-fcos2.md diff --git a/posts/proxmox-fcos2.md b/posts/proxmox-fcos2.md new file mode 100644 index 0000000..2cbd6e1 --- /dev/null +++ b/posts/proxmox-fcos2.md @@ -0,0 +1,711 @@ +--- +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 = <