- Jinja 53.3%
- Python 39.9%
- HCL 4.5%
- Shell 2.3%
## Ce que la page disait, et pourquoi c'est dangereux
Le README du rôle `openbao` décrit le dernier geste du démontage des coffres de nœuds : révoquer sur `ca1` les quatre `secret_id` de l'AppRole `dn42-node-cert` qui appartenaient aux **nœuds**. Il affirmait qu'ils étaient *« distinguables de ceux des conteneurs par leur métadonnée, écrite à l'amorçage »*.
Ils ne le sont pas. La métadonnée ne porte que `{"node": "fr-rbx1"}` — rien sur la nature de l'hôte. Il y a **huit** accessors pour quatre nœuds, et chaque nœud apparaît donc **deux fois** :
```
08097782-… {"md":{"node":"fr-rbx1"},"cidr":[],"created":"2026-08-07T21:59:22Z"}
57d3c9ff-… {"md":{"node":"fr-rbx1"},"cidr":[],"created":"2026-08-09T21:40:04Z"}
```
Suivre la phrase à la lettre revient à choisir au hasard entre les deux. Et la panne est silencieuse : le certificat en place reste valide, l'erreur n'apparaît qu'au renouvellement suivant, **90 jours plus tard**, loin de sa cause.
## Ce qui apparie réellement
La date de création côté `ca1` et le `mtime` du fichier `secret-id` du conteneur, à quelques secondes l'un de l'autre et dans le même ordre :
| | `ca1` (création) | conteneur (`mtime` du fichier) |
|---|---|---|
| bod2 | 21:40:03.264 | 21:40:07.806 |
| bod1 | 21:40:03.285 | 21:40:07.915 |
| rbx2 | 21:40:04.142 | 21:40:10.902 |
| rbx1 | 21:40:04.080 | 21:40:10.979 |
Les quatre autres datent du 07/08 entre 21:59 et 22:08, espacés de plusieurs minutes — l'amorçage manuel nœud par nœud, dix minutes après le merge de la #12. Ce sont eux, les nœuds.
⚠ La seule chronologie git n'aurait pas suffi à trancher : les `secret_id` du 09/08 sont **antérieurs** au commit qui pose le coffre en conteneur (`
|
||
|---|---|---|
| .ci | ||
| ansible | ||
| dns | ||
| netbox | ||
| registry | ||
| tofu | ||
| topology | ||
| .gitignore | ||
| .yamllint | ||
| CLAUDE.md | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
infra-dn42
Infrastructure du réseau DN42 AS4242421607 (thystips.dn42).
Monorepo : provisioning, configuration et DNS partagent une seule source de vérité
(topology/), ce qui rend impossible la divergence d'adressage entre les trois outils.
Reconstruction complète (greenfield). Remplace
ansible-dn42, conservé en référence.
Layout
topology/ SOURCE DE VÉRITÉ : AS, adressage, sites, nœuds, anycast
tofu/ OpenTofu — provisionne les VMs (Proxmox + CloudStack)
ansible/ Ansible — configure les nœuds (inventaire dérivé de topology/)
dns/ vérifie que la zone de management publiée correspond à topology/
.ci/ checks agnostiques du forge (lint + dry-run)
Ajouter un nœud = une seule PR atomique touchant topology/ puis les trois outils.
uv sync # outillage pinné (ansible, lint)
uv run ansible-inventory --graph --vars # topologie telle que la voient les rôles
./.ci/lint.sh # tous les checks
| Outil | Lit | Produit |
|---|---|---|
tofu/ |
nodes.vm, sites |
les VMs |
ansible/ |
nodes, as, anycast |
inventaire + config (Bird, WireGuard, VRF, DNS) |
dns/ |
as.domain_mgmt, nodes.endpoint |
un contrôle de cohérence, aucun enregistrement |
Un producteur par zone, et aucun n'est ici : thystips.cloud appartient au dépôt
ATNET/dns (OctoDNS → Google Cloud DNS, réconcilié au push sur main), thystips.dn42
au rôle dns_auth qui livre un fichier de zone signé à Knot. dns/ se contente de
vérifier que le plan de management publié dit bien la même chose que topology/ —
ansible_host en dépend entièrement.
Architecture
Principe directeur : Bird 3, un seul démon, une VRF. Pas d'EVPN, pas de FRR, pas de VXLAN.
fr-rbx1 fr-rbx2 (OVH/Proxmox, v4+v6) fr-bod1 fr-bod2 (Maison/CloudStack, v6)
│ │ │ │
└────────┴──── mesh WireGuard full-mesh ─┴────────┘
6 tunnels, calculés depuis topology/
endpoints = IP publiques
MTU 1420 · BFD · link-local v6
│
Bird 3 : Babel(RTT) + iBGP + eBGP
| Plan | Techno | Rôle |
|---|---|---|
| Underlay | mesh WireGuard + Babel (métrique RTT) + BFD | joignabilité des loopbacks, reroute latency-aware |
| Interne | iBGP full-mesh entre loopbacks v6 | routes DN42 entre sites, next hop self, v4 en ENH |
| Edge | eBGP par tunnel WireGuard | peers DN42 : ROA, communities, blackhole, transit complet |
Isolation — tout le DN42 (loopbacks, mesh, peers, Bird, services) vit dans la VRF dn42.
La table globale est réservée au management hors-bande (SSH par IP publique, scrape
monitoring) : elle ne contient aucun Bird, ce qui garantit qu'une panne de l'overlay ne
coupe ni l'accès ni la supervision.
Adressage — voir topology/as.yml. IPv6 abondant ; l'IPv4 (/27) est rare, donc
extended-next-hop obligatoire : aucune IPv4 n'est consommée par les tunnels, et les
peers qui ne le supportent pas sont refusés.
Stack
| OS | Debian 13 (Trixie) — bird3 natif |
| Routage | Bird 3 (Babel + iBGP + eBGP dans un seul démon) |
| Réseau | systemd-networkd (.netdev/.network) — WireGuard, VRF, dummy, enslavement atomique au boot |
| Firewall | nftables (table globale + politique VRF) |
| DNS | Knot (autoritatif) et Unbound (récursif) répartis sur des routeurs distincts, une IP anycast par service ; la zone de management clearnet est publiée par le dépôt ATNET/dns |
| RPKI | cache RTR par nœud (StayRTR sur la loopback) — aucun SPOF de filtrage |
| Services | LXC sur la VM du routeur, rattachés à la VRF par veth |
| Secrets | Ansible Vault inline (encrypt_string) |
| Provisioning | OpenTofu + cloud-init |
| Outillage Python | uv (pyproject.toml + uv.lock) — mêmes versions au poste et en CI |
| CI | ansible-lint, yamllint, validation de l'inventaire, --syntax-check, --check --diff |
Choix structurants
- Pas d'EVPN/VXLAN. Les routes DN42 voyagent dans l'iBGP de Bird, jamais injectées en Type-5 : cela préserve les communities et l'AS_PATH, et évite les boucles de redistribution. L'EVPN se retrouvait alors vidé de sa substance — pour un coût de −50 octets de MTU sur tout le transit. Supprimé. (EVPN étudié dans un lab séparé.)
- Un seul IGP, Babel, choisi pour sa métrique RTT (native sur les interfaces tunnel) : adapté à un mesh WireGuard à latence variable, là où BGP-only ne l'est pas.
- Bird plutôt que FRR : le
babeldde FRR n'implémente pas la métrique RTT. Bird la possède depuis 2.14, et sait aussi gérer les VRF et tables kernel multiples — un seul démon détient donc toute la RIB, ce qui supprime la frontière zebra/bird. - Agrégats originés sur tous les nœuds bordure : pas de point unique d'annonce.
- Route reflectors : full-mesh iBGP jusqu'à ~8 nœuds, puis deux RR (jamais un seul).
- Monitoring hors-bande + au moins une sonde externe à l'infrastructure : une supervision in-band s'éteint avec le réseau qu'elle surveille.
- Services DNS répartis par routeur : un autoritatif par site, de sorte que le service survive à la perte d'un site sans faire tourner tout le monde partout. Les deux services ont des IP anycast distinctes — les mêler exposerait un résolveur ouvert.
- Le récursif, lui, tourne sur les quatre nœuds, et pas par goût de la redondance : un
nœud n'atteint la VRF DN42 que par une traversée adressée hors de DN42, donc il ne peut
résoudre
.dn42qu'auprès d'un résolveur local. Le rôle suit le besoin du nœud, pas le découpage par site. Voiransible/README.md. - Code et commentaires en anglais, y compris dans les YAML de
topology/.
État
Le cœur de routage tourne sur les 4 nœuds (fr-rbx1/fr-rbx2 sur Proxmox à Roubaix,
fr-bod1/fr-bod2 sur CloudStack à la maison) : base, network, nftables, bird et
rpki sont déroulés et idempotents. Mesh WireGuard complet, Babel à 3 voisins par nœud avec
métrique RTT, iBGP full-mesh 12/12 établi, et une session RTR établie par nœud contre son
propre cache — les tables ROA sont pleines, donc le filtre d'import ne laisse plus tout passer.
Restent écrits mais non déroulés : wireguard_peers, dns_auth, dns_rec.
Le déploiement est progressif : un nœud dont la clé publique n'est pas encore connue est
ignoré, sans rien changer à la configuration des autres. Aucun peering DN42 externe n'est
repris de l'ancienne infrastructure (cf. ansible/legacy-peerings.md).
Le monitoring est écrit — rôle monitoring, en push vers la stack Mimir/Loki existante,
par le plan privé. Ce qui manque encore pour satisfaire l'exigence ci-dessus est la sonde
externe : tant qu'elle n'existe pas, une panne du site qui héberge Grafana emporte la
supervision en silence, ce qui est précisément le mode de panne contre lequel elle existe.