Projet 8: un cloud où la seule autorité est l'historique git signé
Le projet 8 (Apache-2.0, v0.6.0) est parti d’une question inconfortable: pourquoi chaque “cloud self-hosted” a-t-il encore besoin d’un contrôleur, d’un protocole de quorum et d’un service de tokens pour fonctionner? La réponse dans le design de 8: il n’en a pas besoin. Chaque nœud est souverain et égal. Il rejoint un mesh, pull son état désiré depuis un dépôt git signé, vérifie les signatures et converge localement. Pas de maître, pas de token de consensus, pas de blockchain. L’autorité vit dans l’historique git signé Ed25519 ou elle ne vit nulle part.
La pile, couche par couche
Le README porte un mermaid à cinq couches; les pièces qui portent:
- L1, le mesh: l’overlay IPv6 Yggdrasil
. Chaque nœud reçoit une adresse stable, chiffrée, auto-réparatrice et l’identité du nœud est une clé, pas une IP. La table de routage de la gateway n’accepte que des adresses de forme Yggdrasil (
0200::/7): les entrées malformées sont sautées, jamais proxyfiées vers loopback ou link-local. C’est une garde SSRF cachée dans une règle de validation. - L2, la source de vérité: Radicle
(git P2P), repo
rad:z4GctVJZH9WcgG23xVx2kSfieapPP. Les nœuds le pullent et le clonent par le mesh; le daemon tourne en unit systemd user avec peering NID. Rien n’est jamais poussé vers un nœud par un acteur privilégié: le flux est pull-only par construction (ADR-0003). - L3, l’agent:
plan → diff → apply. Un prototype Python 3.12 d’abord (76% à 89% de couverture), puis un portage Rust (ADR-0010/0018) livré en binaire statique à 81% de couverture de lignes, avec des tests de parité py+rs pour chaque comportement: apply dry-run vs vrai rapport, génération des args runtime podman /k0s, relais de commits signés avec trailers d’attribution et rollback si le push ne converge pas. - L4, les workloads: Podman rootless plus un cluster k0s single-node par machine (
--single: kine+SQLite, pas de plan de contrôle partagé, ADR-0007). Le k8s de chaque nœud ne dépend de rien d’autre; le mesh est le seul cluster. - La sécurité de L2, la partie fail-closed:
eight applysynchronise le clone Radicle et vérifie chaque nouvelle signature de commit contrestate/trusted-keys/avant de converger; un échec de vérification sort en code 2, le nœud garde son dernier bon état. L’api construit aussi son fichierallowed_signersdepuis un seul instantané des clés (la double lecture originelle était une TOCTOU attendant de compter).
Bootstrap day ou les vingt bugs de la première convergence
L’entrée v0.5.0 du CHANGELOG est une ligne et une leçon: “Mesh bootstrap green (2026-08-29)”, avec ansible-playbook site.yml qui converge trois VM libvirt à failed=0 après ~20 bugs de première course sur 9 roles et l’inventaire. Ceux qu’il faut lire deux fois:
- cloud-init lie la config réseau au MAC du premier boot, donc des MAC aléatoires orphelinaient les nœuds: les MAC des NIC sont maintenant pinées (
52:54:00:8a:00:0X) et les réservations dnsmasq rendent IPs et DNS stables entre les applies. - Deux instances dnsmasq ne partagent pas un sous-réseau: le réseau
defaultstock de libvirt possède déjà192.168.122.0/24, donc eight-net a déménagé vers192.168.123.0/24. - L’AppArmor d’Ubuntu 24.04 tue QEMU sur les disques
type='volume'(virt-aa-helper de libvirt 10.0.0 ne résout pas les références de volumes,Permission denied): workaround documenté dans le quickstart, parce que “ça marche sur ma machine” est une promesse que le prochain bootstrap day viendra collecter. yggdrasilctlsans-jsonretourne une table, qui parsait en vide: l’API des peers ne rapportait rien sur un mesh pourtant up.- Le stdenv de NixOS strippait l’overlay du binaire k0s: un binaire sans payload, “no payload attached”, sur le nœud déclaratif.
Aucun de ces bugs n’est exotique pris isolément; le vingtième, c’est là qu’un homelab grandit soit en discipline de documentation, soit meurt d’en manquer.
Les outils ennuyeux, délibérément
Le provisionnement est OpenTofu + Ansible contre un inventaire dynamique libvirt; le nœud NixOS est cuit en qcow2 avec overlays et déployé par colmena (ADR-0014); les secrets voyagent via fnox avec des clés age; la supervision est une pile VictoriaMetrics/VictoriaLogs/Grafana par nœud composée en kustomize helmCharts (ADR-0019) et des agents Wazuh FIM sur chaque nœud (ADR-0020, NixOS via un agent natif enveloppant le .deb officiel). 23 ADRs enregistrent chacun de ces choix, y compris les rejetés. La règle des licences est écrite dans un ADR aussi (0004): uniquement des licences libres OSI, pas de runtime exotique sur lequel parier.
La seule autorité, c’est l’historique
- Pull-only, c’est tout le modèle de sécurité. Un nœud qui ne pulle que de l’historique signé n’a besoin d’aucune autorité entrante, d’aucun token, d’aucune confiance dans le réseau où il est posé.
- Les bugs de bootstrap sont des bugs de config en costumes. Pinning MAC, collisions de sous-réseaux, AppArmor, flags JSON: rien n’a échoué dans le nouveau code; tout a échoué aux frontières entre outils.
- Prototyper en Python, porter en Rust, garder les tests de parité. La suite de parité, c’est ce qui fait du portage un refactor plutôt qu’une réécriture.
- La gateway AI est un service HTTP normal. Le streaming SSE proxyfié depuis un thread détaché (un consommateur lent ne doit pas bloquer la boucle accept), des corps bornés à 16 MiB, du routage Yggdrasil-only. La partie AI du design tient en trois paragraphes; la partie fiabilité remplit le CHANGELOG.