Expérimentation de Fedora CoreOS sur Proxmox : création de la machine virtuelle d'installation

Ce billet est le deuxième d’une série traitant de la création d’une infrastructure virtualisée à l’aide de Proxmox1 pour la partie hyperviseur et de Fedora CoreOS2 pour le système d’exploitation des machines virtuelles invitées (guests). L’infrastructure codifiée (Infrastructure as Code) est réalisée avec OpenTofu3 (Hashicorp Terraform ayant rejoint le côté obscur de la Force).

Dans le premier billet4, 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 5.

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 iPXE6, 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.

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 namespaces7 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.

Cette distribution permet bien d’installer des logiciels additionnels sur le socle, grâce à un système de surcouches (layering) de rpm-ostree8. 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.

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 OpenShift9.

De même, les volumes des conteneurs ne sont qu’assez rarement des bind-mount10 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 SELinux11 qui restreignent les interactions avec les fichiers dans les volumes, qui doivent avoir (principalement) les types container_file_t ou container_ro_file_t12, ce qu’ont rarement les fichiers du socle.

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’initialisation13, à 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 ConfigMap14. 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.

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 Quadlets15. 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.

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:16.

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 Packer17.

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 Ignition18, qui a ensuite été abandonné, puis repris par la communauté19. Ce fournisseur propose des sources de données (data sources) afin de structurer la configuration Terraform et la typer.

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’Ignition20.

Avec le fournisseur, on écrit donc :

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 :

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ême21. Par exemple la structure racine d’une configuration Ignition est définie ainsi :

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é schematyper22. Cet outil prend en entrée une description de données au format JSON Schema23, et écrit en sortie des définitions de structures en Go.

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 chroot24.

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_file25 ou même une null_resource26, puis téléversées grâce au mécanisme d’approvisionnement (provisioner) “file”27. Le code aurait ressemblé à :

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 SSH28

La preuve de concept actuelle repose donc sur plusieurs mécanismes d’approvisionnement, dont local-exec29 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 WebDav30, afin de palier cette situation.

Problèmes avec le fournisseur Opentofu pour Proxmox

Le fournisseur Opentofu pour Proxmox bpg/proxmox31 est le fournisseur le plus avancé disponible sur les registres de Terraform.

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 = <<EOT
curl \
  -F "content=iso" \
  -F "filename=@customized-${random_pet.config_name.id}.iso;type=application/vnd.efi.iso;filename=fcos-netboot-server-${random_pet.config_name.id}.iso" \
  -H "@${local_file.api_token.filename}" \
  "${var.pve_api_base_url}nodes/${var.pve_node_name}/storage/${var.pve_storage_id}/upload"
EOT      
    }

Une ressource de type local_file a également été utilisée pour stocker le jeton d’API, de manière à éviter d’exposer le jeton directement sur la ligne de commandes32.

Problèmes avec systemd.path et la détection de changements de fichiers dans un répertoire

Le serveur DHCP utilisé par cette preuve de concept repose sur dnsmasq. Celui-ci contient certainement moins de fonctionnalités que le serveur de l’ISC, mais il convient parfaitement pour le démarrage par le réseau.

Une fonctionnalité manquante sur ces deux logiciels est la capacité de recharger automatiquement la configuration lors de son changement ou de son extension. Or, cette preuve de concept repose sur l’extensibilité de la configuration par Opentofu : lorsqu’une nouvelle machine est ajoutée à l’infrastructure, des options DHCP sont définies dynamiquement pour permettre son installation. En particulier, une option DHCP indique où trouver le fichier de configuration Ignition spécifique à cette machine à installer et une autre indique le chemin du disque dur sur lequel effectuer l’installation.

dhcp-host=${mac_address},set:${hostname}tag,${host_ip},${hostname}
dhcp-option=tag:${hostname}tag,encap:128,2,"${vm_id}.ign"
dhcp-option=tag:${hostname}tag,encap:128,3,"/dev/disk/by-path/pci-0000:00:0a.0"

Le redémarrage du service pourrait être fait avec un mécanisme d’approvisionnement de type remote-exec33, mais cette solution nécessite la capacité d’exécuter des commandes sur le socle depuis Opentofu.

Opentofu dispose déjà de la capacité d’exécuter des commandes arbitraires sur toutes les machines de l’infrastructure par injection de commandes et de fichiers dans les configurations Ignition. Cela demande néanmoins le redémarrage de la machine virtuelle pour appliquer la nouvelle configuration, ce qui n’est pas forcément très discret pour un attaquant qui souhaiterait ainsi compromettre des machines.

Une solution plus propre et ne nécessitant pas d’accès shell est de surveiller les fichiers de configuration avec inotify34. Inotify est une fonctionnalité du noyau Linux qui permet d’émettre des événements lors d’opérations sur le système de fichiers. Les programmes intéréssés peuvent s’abonner à ces événements en vue d’y réagir.

Systemd peut être configuré pour surveiller le système de fichiers avec inotify, grâce aux unités de type path35. La configuration ressemble à ceci :

[Unit]
Description = Path Monitor for DHCP Config

[Path]
PathChanged=/path/to/monitored/path
TriggerLimitIntervalSec=0

[Install]
WantedBy=multi-user.target

Cette unité d’exemple active un service du même nom que celui de l’unité de type path lorsque le chemin indiqué subit une modification. Ce chemin peut être un fichier ou un répertoire. Dans le cas d’un répertoire, les changements n’interviennent que lors de la suppression ou de l’ajout d’un fichier ; les opérations de modification des fichiers contenus dans un répertoire n’entrainent pas de modification du répertoire lui-même.

Hélas, bien qu’il existe une directive PathExistsGlob permettant l’usage de chaines de substitution (e.g. *.conf), il n’existe aucune directive de type PathChangedGlob qui permettrait la surveillance de tous les fichiers au sein d’un répertoire. Un ticket à ce sujet est ouvert depuis plusieurs années36. Un bricolage pour s’accomoder de la situation est de supprimer les fichiers de configuration existants avant d’ajouter les nouveaux, de façon à provoquer un ou deux redémarrages du service37 et la prise en compte de la nouvelle configuration.

Un autre défaut de cette solution est qu’elle exige que systemd “espionne” les fichiers déposés par SFTP dans un volume géré par Podman. Cela implique de faire une hypothèse sur les chemins utilisés par Podman ou de monter une deuxième fois le système de fichiers associé à ce volume à un chemin connu du socle. Si le montage peut sembler plus propre, il faut considérer que lorsqu’on monte un même système de fichiers à de multiples endroits, il faut que les options de montage soient identiques, y compris les informations relatives à SELinux (e.g. l’option rootcontext). Il faut alors à nouveau faire des hypothèses : les options utilisées par les Quadlets Podman lors du montage du système de fichiers sur le volume.

Dans le ticket précédemment évoqué, un moyen de contournement est évoqué : utiliser l’utilitaire inotifywatch38. Ce programme permet la surveillance de plusieurs fichiers dans un répertoire, nommés suivant un motif spécifié. Hélas, ce dernier n’est pas disponible sur le socle de Fedora CoreOS, et comme expliqué précédemment, il n’est pas possible d’installer des logiciels additionnels dans notre cas d’usage. Il est cependant possible de l’utiliser dans un conteneur qui aurait également accès au volume des extensions de configuration DHCP. Une pierre, deux coups : on contournerait la limitation de systemd et on ne ferait plus d’hypothèse sur le chemin d’accès ! Hélas, il reste ensuite à trouver un moyen de redémarrer le service dnsmasq depuis le conteneur ayant détecté le changement…

Donner un accès SSH sur le socle depuis le conteneur de surveillance semble la voie royale. Hélas, cet accès est compliqué à donner, du fait des politiques SELinux, encore une fois, ou du plan d’adressage dynamique de la couche réseau de Podman.

L’hypothèse à faire sur le plan d’adressage de Podman est de connaitre l’adresse IP associée au socle. Elle doit être faite si on tente de se connecter en SSH au socle par le réseau. Une solution pour éviter de faire des hypothèses sur la couche réseau est d’établir la connexion SSH à travers une socket Unix bind-montée dans le conteneur de surveillance. Exposer un service SSH sur socket Unix à d’autres conteneurs ou utilisateurs est une astuce assez élégante notamment discutée par Timothée Ravier, développeur de CoreOS, sur son blog dans le cadre d’un remplacement de sudo par une connexion SSH locale39. Hélas, dans le cas d’espèce, les politiques SELinux empêchent la connexion de socat, utilisé par le client SSH du conteneur, à la socket Unix exposée par le serveur SSHD du socle.

En conséquence, sur cette problématique, aucune solution satisfaisante n’a été trouvée. La solution retenue est, en attendant de trouver mieux, d’utiliser une unité systemd de type path en faisant l’hypothèse sur le chemin du volume Podman, et en supprimant puis en rajoutant le fichier de configuration, afin de déclencher le redémarrage du service.

Conclusion

Cet article a couvert une partie des problèmes rencontrés et des solutions proposées pour la création d’une preuve de concept d’un serveur d’installation pour une infrastructure basée sur Fedora CoreOS sur Proxmox. Le code est ouvert5 et les commentaires sont les bienvenues, afin d’améliorer cette dernière et permettre à la communauté d’en bénéficier.

Grâce à cette machine d’installation, il devient trivial de déployer de nouvelles machines faisant tourner Fedora CoreOS. Il suffit pour cela de téléverser le fichier Ignition de la machine à installer et un fichier d’extension de la configuration de dnsmasq, puis de configurer la nouvelle machine virtuelle pour démarrer par le réseau.

Ces téléversements peuvent s’effectuer lors de la déclaration de la nouvelle machine virtuelle dans la configuration d’Opentofu.

Dans le prochain billet de cette série, nous verrons comment déployer, grâce à cette machine d’installation, un serveur DNS40, un serveur ACME41, et un cluster etcd42, en vue d’installer une instance d’Openbao43, et ainsi pouvoir enfin créer des instances de Fedora CoreOS sans placer de secrets dans les fichiers Ignition.


  1. https://www.proxmox.com/en/ ↩︎

  2. https://fedoraproject.org/coreos/ ↩︎

  3. https://opentofu.org/ ↩︎

  4. https://www.broken-by-design.fr/posts/proxmox-fcos1/ ↩︎

  5. https://git.broken-by-design.fr/fmaury/iac ↩︎ ↩︎

  6. https://ipxe.org/ ↩︎

  7. https://www.man7.org/linux/man-pages/man7/namespaces.7.html ↩︎

  8. https://coreos.github.io/rpm-ostree/ ↩︎

  9. https://www.redhat.com/fr/technologies/cloud-computing/openshift ↩︎

  10. https://docs.docker.com/storage/bind-mounts/ ↩︎

  11. https://github.com/containers/container-selinux/blob/main/container_selinux.8 ↩︎

  12. https://github.com/containers/container-selinux/blob/main/container.te ↩︎

  13. https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ ↩︎

  14. https://kubernetes.io/docs/concepts/configuration/configmap/ ↩︎

  15. https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html ↩︎

  16. https://www.rfc-editor.org/rfc/rfc2397 ↩︎

  17. https://www.packer.io/ ↩︎

  18. https://github.com/hashicorp/terraform-provider-ignition ↩︎

  19. https://github.com/community-terraform-providers/terraform-provider-ignition ↩︎

  20. https://coreos.github.io/ignition/configuration-v3_4/ ↩︎

  21. https://github.com/coreos/ignition/blob/v2.18.0/config/v3_4/types/schema.go ↩︎

  22. https://github.com/idubinskiy/schematyper ↩︎

  23. https://json-schema.org/ ↩︎

  24. 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. ↩︎

  25. https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file ↩︎

  26. https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource ↩︎

  27. https://developer.hashicorp.com/terraform/language/resources/provisioners/file ↩︎

  28. Le protocole SCP est devenu obsolète à la suite d’une vulnérabilité protocolaire irréparable : https://lwn.net/Articles/835962/ ↩︎

  29. https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec ↩︎

  30. https://www.rfc-editor.org/rfc/rfc4918 ↩︎

  31. https://registry.terraform.io/providers/bpg/proxmox/latest/docs ↩︎

  32. https://cwe.mitre.org/data/definitions/214 ↩︎

  33. https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec ↩︎

  34. https://www.man7.org/linux/man-pages/man7/inotify.7.html ↩︎

  35. https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html ↩︎

  36. https://github.com/systemd/systemd/issues/14330 ↩︎

  37. L’incertitude entre un ou deux redémarrage provient du fait qu’inotify ne garantit pas un événement distinct par opération recherchée. Si deux événements de même type se produisent avant qu’un observateur/abonné ne soit informé du premier, un unique événement lui est rapporté. ↩︎

  38. https://www.man7.org/linux/man-pages/man1/inotifywatch.1.html ↩︎

  39. https://tim.siosm.fr/blog/2023/12/19/ssh-over-unix-socket/ ↩︎

  40. https://www.knot-resolver.cz/ ↩︎

  41. https://caddyserver.com/docs/caddyfile/directives/acme_server ↩︎

  42. https://etcd.io/ ↩︎

  43. https://openbao.org/ ↩︎