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