Add second post on FCOS on proxmox
This commit is contained in:
parent
cba601728f
commit
58607f70e4
1 changed files with 711 additions and 0 deletions
711
posts/proxmox-fcos2.md
Normal file
711
posts/proxmox-fcos2.md
Normal file
|
@ -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 = <<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
|
||||||
|
commandes[^cmdline_exposed].
|
||||||
|
|
||||||
|
[^cmdline_exposed]: https://cwe.mitre.org/data/definitions/214
|
||||||
|
|
||||||
|
### 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-exec[^remote_exec], mais cette solution
|
||||||
|
nécessite la capacité d'exécuter des commandes sur le socle depuis Opentofu.
|
||||||
|
|
||||||
|
[^remote_exec]: https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec
|
||||||
|
|
||||||
|
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`[^inotify]. 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.
|
||||||
|
|
||||||
|
[^inotify]: https://www.man7.org/linux/man-pages/man7/inotify.7.html
|
||||||
|
|
||||||
|
Systemd peut être configuré pour surveiller le système de fichiers avec `inotify`,
|
||||||
|
grâce aux unités de type `path`[^systemd_path]. La configuration ressemble à ceci :
|
||||||
|
|
||||||
|
[^systemd_path]: https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html
|
||||||
|
|
||||||
|
```
|
||||||
|
[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ées[^pathchangedglob]. 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 service[^coalesce] et la prise
|
||||||
|
en compte de la nouvelle configuration.
|
||||||
|
|
||||||
|
[^pathchangedglob]: https://github.com/systemd/systemd/issues/14330
|
||||||
|
[^coalesce]: 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é.
|
||||||
|
|
||||||
|
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`[^inotifywatch]. 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...
|
||||||
|
|
||||||
|
[^inotifywatch]: https://www.man7.org/linux/man-pages/man1/inotifywatch.1.html
|
||||||
|
|
||||||
|
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 locale[^sshunix].
|
||||||
|
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.
|
||||||
|
|
||||||
|
[^sshunix]: https://tim.siosm.fr/blog/2023/12/19/ssh-over-unix-socket/
|
||||||
|
|
||||||
|
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
|
||||||
|
ouvert[^sourcecode] 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 DNS[^knot_resolver], un serveur
|
||||||
|
ACME[^caddy], et un cluster etcd[^etcd], en vue d'installer une instance
|
||||||
|
d'Openbao[^openbao], et ainsi pouvoir enfin créer des instances de Fedora CoreOS
|
||||||
|
sans placer de secrets dans les fichiers Ignition.
|
||||||
|
|
||||||
|
[^knot_resolver]: https://www.knot-resolver.cz/
|
||||||
|
[^caddy]: https://caddyserver.com/docs/caddyfile/directives/acme_server
|
||||||
|
[^etcd]: https://etcd.io/
|
||||||
|
[^openbao]: https://openbao.org/
|
Loading…
Reference in a new issue