feat(incus) : l'API d'administration, sur le plan de management #21

Merged
thystips merged 5 commits from feat/incus-api into main 2026-08-08 19:31:27 +02:00
Owner

Un refus repris, et je le dis franchement : l'API Incus avait été écartée le 08/08 au motif d'« une API d'administration de plus sur des routeurs de bordure ». L'analyse confondait deux choses — exposer une API sur DN42, qui reste hors de question, et en exposer une là où sshd écoute déjà et où Ansible arrive déjà.

Elle est nécessaire, pas confortable : le connecteur Ansible incus exécute incus exec depuis le contrôleur. Sans elle, un conteneur ne peut pas être un hôte d'inventaire, donc les rôles déjà écrits ne s'y appliquent pas — tout l'objet du passage aux conteneurs.

Deux garde-fous plutôt qu'un : le démon se lie à l'adresse de management et jamais au wildcard (que tcp_l3mdev_accept=1 rendrait joignable depuis l'overlay), et la règle restreint aux deux plans privés — pas à nftables_ssh_sources_*, qui valent « partout » par défaut.

Le certificat client n'a que sa moitié publique dans le dépôt, comme la chaîne d'AC du rôle openbao. La confiance se compare par empreinte, pas par nom : un certificat remplacé sous le même nom serait sinon tenu pour déjà accordé, et le poste refoulé sans explication.

Mesuré : les quatre nœuds répondent auth: trusted, la joignabilité du plan de management depuis la maison ayant été vérifiée avant d'ouvrir quoi que ce soit (14,3 ms vers Roubaix, 0,65 ms vers Bordeaux). Écoute confirmée sur <mgmt>:8443. changed=0 au second passage.

Ligne 19 de la matrice des flux, avec le raisonnement complet.

**Un refus repris, et je le dis franchement** : l'API Incus avait été écartée le 08/08 au motif d'« une API d'administration de plus sur des routeurs de bordure ». L'analyse confondait deux choses — exposer une API **sur DN42**, qui reste hors de question, et en exposer une **là où `sshd` écoute déjà et où Ansible arrive déjà**. Elle est **nécessaire, pas confortable** : le connecteur Ansible `incus` exécute `incus exec` depuis le contrôleur. Sans elle, un conteneur ne peut pas être un hôte d'inventaire, donc les rôles déjà écrits ne s'y appliquent pas — tout l'objet du passage aux conteneurs. **Deux garde-fous plutôt qu'un** : le démon se lie à l'adresse de management et **jamais au wildcard** (que `tcp_l3mdev_accept=1` rendrait joignable depuis l'overlay), et la règle restreint aux deux plans privés — pas à `nftables_ssh_sources_*`, qui valent « partout » par défaut. Le certificat client n'a que sa **moitié publique** dans le dépôt, comme la chaîne d'AC du rôle openbao. La confiance se compare par **empreinte**, pas par nom : un certificat remplacé sous le même nom serait sinon tenu pour déjà accordé, et le poste refoulé sans explication. **Mesuré** : les quatre nœuds répondent `auth: trusted`, la joignabilité du plan de management depuis la maison ayant été vérifiée **avant** d'ouvrir quoi que ce soit (14,3 ms vers Roubaix, 0,65 ms vers Bordeaux). Écoute confirmée sur `<mgmt>:8443`. `changed=0` au second passage. Ligne 19 de la matrice des flux, avec le raisonnement complet.
Le démon, un magasin et un profil. Rien ne tourne dedans — c'est le socle, pas encore
un service déplacé.

Le paquet Debian, jamais le dépôt amont : trixie fournit 6.0.4, la branche LTS, et
c'est cette contrainte qui rend acceptable de poser un gestionnaire de conteneurs sur
un routeur de bordure. Ce que ça coûte est assumé et documenté — les images OCI
demandent 6.3, donc elles sont hors de portée, ce qui n'est pas une perte puisque nos
services viennent en grappes et non en processus uniques.

Trois choix qui gardent le nœud tel qu'il était :

- AUCUN réseau managé. Les conteneurs se branchent sur `br-svc`, déjà asservi à la VRF
  et déjà annoncé par Bird. Vérifié après installation : `nft list tables` ne montre
  toujours que `inet dn42`, Incus n'a rien écrit — sa table n'existe que pour ses
  propres ponts NAT.
- AUCUNE API réseau. Pas de `core.https_address` : la socket unix et rien d'autre.
  Une API d'administration sur une bordure, ce serait une ligne de matrice, un
  certificat client et un port à défendre, pour une ergonomie qu'Ansible fournit déjà.
- Magasin `dir`. Le retour arrière s'obtient par remplacement, les conteneurs étant
  jetables, donc aucune couche de stockage nouvelle.

Pas de `incus admin init --preseed` : il initialise, et rejoué contre un serveur qui a
déjà son magasin il refuse — il ne pouvait donc pas être le régime permanent d'un rôle
qui doit converger à changed=0. Le rôle lit, compare et n'écrit que si ça diffère,
comme `openbao_config` avec `uri`.

MESURÉ de bout en bout, avec un conteneur jetable détruit depuis :

- la question ouverte est tranchée — `community.general.lxd_storage_pool_info` parle
  au socket d'Incus via `url: unix:/var/lib/incus/unix.socket`, donc les modules `lxd_*`
  suffisent et il n'y aura pas à piloter la CLI à la main ;
- un conteneur sur `br-svc` joint TOUT DN42 par simple routage : sa loopback distante
  à 13,3 ms, et un pair DN42 externe à 5,0 ms. Il est joignable depuis l'autre site à
  13,7 ms. C'est la propriété qui fera disparaître les contournements de VRF ;
- le pare-feu n'a rien demandé : `iifname @dn42_ifaces oifname @dn42_ifaces accept`
  couvre déjà le cas, `br-svc` étant dans l'ensemble ;
- l'empreinte : 86 Mo pour le démon, 22 Mo pour un conteneur Debian au repos — la
  moitié des 40 Mo estimés — et 100 Mo d'image en cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Elle avait été refusée le 08/08 et elle est reprise ici, parce que le refus confondait
deux choses très différentes : exposer une API SUR DN42, qui reste hors de question,
et en exposer une là où `sshd` écoute déjà et où Ansible arrive déjà. C'est le second
cas, et la distinction manquait à l'analyse d'hier.

Elle est nécessaire, pas confortable : le connecteur Ansible `incus` exécute
`incus exec` DEPUIS le contrôleur. Sans API joignable, un conteneur ne peut pas être un
hôte d'inventaire — et les rôles déjà écrits ne s'y appliqueraient donc pas, ce qui
était tout l'objet du passage aux conteneurs.

Deux garde-fous plutôt qu'un. Le démon se lie à l'adresse de MANAGEMENT et jamais au
wildcard, que `tcp_l3mdev_accept=1` rendrait joignable depuis l'overlay ; et la règle
restreint aux deux plans privés — pas à `nftables_ssh_sources_*`, qui valent « partout »
par défaut. Vérifié après déploiement : l'écoute est bien sur `<mgmt>:8443`.

L'authentification est un certificat client dont seule la MOITIÉ PUBLIQUE est dans le
dépôt, comme la chaîne d'AC que porte le rôle openbao ; la clé privée ne quitte pas le
poste. La confiance se compare par EMPREINTE et non par nom : un certificat remplacé
sous le même nom serait sinon considéré comme déjà accordé, et le poste se ferait
refouler sans que rien ne dise pourquoi.

Mesuré : les quatre nœuds répondent `auth: trusted` au poste, la joignabilité du plan
de management depuis la maison ayant été vérifiée AVANT d'ouvrir quoi que ce soit
(14,3 ms vers Roubaix, 0,65 ms vers Bordeaux). `changed=0` au second passage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Un conteneur posé sur `br-svc` vit dans DN42 SEULEMENT : son /64 est sous `fd1d:`, que
personne ne route sur internet, donc ni `apt` ni les binaires épinglés que les rôles
téléchargent n'y arriveraient. C'est la « seconde patte » que le plan mentionnait sans
la spécifier.

Un pont MANAGÉ par Incus, qui NAT vers l'interface publique du nœud. Délégué plutôt
qu'écrit à la main : Incus maintient ses propres règles dans une table `inet incus`,
confinée à ce pont — le pont de services reste non managé et il n'y touche pas. C'est
le premier NAT du dépôt, et il n'est pas de nous.

TROIS PIÈGES, tous rencontrés en déroulant :

- `ipv6.address=auto` ferait tirer à Incus une ULA aléatoire, donc dans `fd00::/8` —
  exactement ce que le pare-feu de ce dépôt appelle DN42. Le garde « l'overlay ne sort
  pas de la VRF » jetterait alors la sortie du conteneur en la prenant pour une fuite.
  D'où `ipv6.address=none` : les conteneurs gardent l'IPv6 pour DN42, sur l'autre patte,
  et ne prennent de l'IPv4 que pour sortir.
- Le NAT d'Incus ne suffit pas seul. Deux tables netfilter coexistent et un paquet doit
  être accepté par les DEUX : sans la ligne ajoutée à notre `forward`, la sortie était
  jetée malgré un NAT parfaitement configuré.
- `dnsmasq-base` doit être nommé explicitement, `install_recommends: false` l'écartant.
  Sans lui Incus refuse de créer le pont avec « Failed to check dnsmasq version », ce
  qui ne dit pas qu'un paquet manque. La variante `-base` et non `dnsmasq` : la complète
  installerait un résolveur sur le nœud, dont le résolveur reste Unbound.

10.42.0.0/24 plutôt qu'un 10.x tiré au sort : 10.1/16 est Roubaix, 10.2/16 la maison et
10.42.255.0/31 la traversée du résolveur. Identique sur les quatre nœuds, ce qui est
sans conséquence derrière un NAT.

MESURÉ de bout en bout, conteneur jetable détruit depuis : ping internet à 4,9 ms,
`apt-get update` puis `apt-get install python3` réussis, et surtout un playbook complet
— `raw` pour amorcer python, `setup` pour les faits, puis un vrai module — exécuté
DEPUIS LE POSTE à travers l'API. Ansible rapporte « Debian 13.6 | virtualisation=lxc
guest ». Les rôles existants pourront donc s'appliquer aux conteneurs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
L'adressage et cloud-init, c'est-à-dire ce qui rend les conteneurs déclaratifs.

`topology/containers.yml` décrit un SERVICE, jamais une instance : le placement suit le
rôle que les nœuds portent déjà, donc un cinquième routeur aura son conteneur sans
qu'une ligne soit écrite. L'adresse en découle :

    <lan de services du site>::<index>:<id du nœud>

Le /64 est partagé par les nœuds d'un site, donc une adresse doit être unique sur DEUX
axes : `index` distingue le service, `id` la machine, et les deux sont déjà stables et
jamais réutilisés. Lire une adresse dit ce qu'elle est et où elle tourne. L'index est
réservé comme as.yml réserve le /27 — l'inventaire refuse un doublon, un index absent,
un rôle que personne ne porte, et un nœud sans patte de services.

Le conteneur naît configuré, par cloud-init injecté par Incus, exactement comme tofu le
fait pour les VM. La variante `cloud` de l'image est nécessaire et non préférée : l'image
ordinaire configure eth0 en DHCP — la convention LXC — alors qu'ici eth0 fait face à
DN42, qui n'a ni DHCP ni annonces de routeur. Sans elle un conteneur démarre sans
aucune adresse, ce qui a été constaté avant d'être corrigé.

DEUX DÉFAUTS TROUVÉS EN DÉROULANT, et le second est le plus grave du lot :

- La même leçon que `forward`, à payer une seconde fois dans `input` : le bail DHCP et
  la résolution sont servis par le dnsmasq du nœud, donc ce trafic lui est DESTINÉ.
  Sans règle, le conteneur n'obtient jamais d'adresse et rien ne le dit — le journal du
  client montre une absence de réponse, pas un refus.
- **`flush ruleset` détruisait la table d'Incus.** Le jeu de règles vidait TOUTES les
  tables de la machine, y compris `inet incus` où vit le NAT de la sortie des
  conteneurs, et Incus ne la recrée pas. Un simple `--tags nftables` coupait donc tous
  les conteneurs du réseau, en silence, le symptôme n'apparaissant qu'au prochain `apt`.
  Remplacé par le motif idiomatique qui ne vide que notre table. Vérifié : après
  rechargement, les deux tables coexistent et le conteneur sort toujours.

Une garde de ce rôle était fausse et a été corrigée : attendre `python3` ne prouvait
rien, l'image `cloud` l'embarquant déjà — cloud-init est écrit en python. Elle
rapportait donc un succès sur un conteneur sans la moindre adresse. C'est
`cloud-init status --wait`, suivi d'un contrôle explicite de la route par défaut.

MESURÉ : quatre conteneurs, adresses `:🅰️1`, `:🅰️3`, `:🅰️2`, `:🅰️4` conformes au
schéma, bail DHCP obtenu sur la patte de sortie, un conteneur de Roubaix joint un
conteneur de Bordeaux PAR DN42 en 14,9 ms, et un nœud tiers les joint dans la VRF en
13,8 ms. `changed=0` au second passage sur les quatre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Aucun changement de contenu : `main` porte le même travail que le premier commit de
cette branche, sous une autre identité. La PR #20 a été fusionnée par ÉCRASEMENT, donc
`9ccc1ec` et `4ead0cc` ont des arbres identiques mais aucun lien de parenté — d'où trois
conflits sur des fichiers que personne n'a modifiés deux fois.

Résolus en gardant cette branche, qui est un sur-ensemble dans les trois cas, et vérifié
plutôt que supposé : `git diff` entre l'arbre fusionné et celui d'avant la fusion est
vide.

La cause est une erreur de ma part, et c'est la quatrième fois que ce dépôt la rencontre :
la branche a été créée depuis `feat/incus` au lieu de repartir de `main`. Après une
fusion par écrasement, `git checkout main && git pull` avant toute nouvelle branche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
thystips deleted branch feat/incus-api 2026-08-08 19:31:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ThysTips_dn42/infra-dn42!21
No description provided.