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.
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_t
12, 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.
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.
Un projet ne serait pas une aventure, sans son lot d’ornières et d’imprévus. Quelle aventure se fut !
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é
schematyper
22. 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.
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_file
25 ou même une null_resource
26, 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-exec
29 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.
Le fournisseur Opentofu pour Proxmox bpg/proxmox
31 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.
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 inotify
34. 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 path
35. 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 inotifywatch
38. 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.
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.
https://www.man7.org/linux/man-pages/man7/namespaces.7.html ↩︎
https://www.redhat.com/fr/technologies/cloud-computing/openshift ↩︎
https://github.com/containers/container-selinux/blob/main/container_selinux.8 ↩︎
https://github.com/containers/container-selinux/blob/main/container.te ↩︎
https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ ↩︎
https://kubernetes.io/docs/concepts/configuration/configmap/ ↩︎
https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html ↩︎
https://github.com/community-terraform-providers/terraform-provider-ignition ↩︎
https://github.com/coreos/ignition/blob/v2.18.0/config/v3_4/types/schema.go ↩︎
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. ↩︎
https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file ↩︎
https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource ↩︎
https://developer.hashicorp.com/terraform/language/resources/provisioners/file ↩︎
Le protocole SCP est devenu obsolète à la suite d’une vulnérabilité protocolaire irréparable : https://lwn.net/Articles/835962/ ↩︎
https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec ↩︎
https://registry.terraform.io/providers/bpg/proxmox/latest/docs ↩︎
https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec ↩︎
https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html ↩︎
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é. ↩︎
https://www.man7.org/linux/man-pages/man1/inotifywatch.1.html ↩︎
https://tim.siosm.fr/blog/2023/12/19/ssh-over-unix-socket/ ↩︎
https://caddyserver.com/docs/caddyfile/directives/acme_server ↩︎