feat/dn42-ingress-tier #12

Merged
thystips merged 9 commits from feat/dn42-ingress-tier into main 2026-08-07 21:48:29 +02:00
Owner
No description provided.
Le besoin n'était pas « exposer le LG » mais publier des services dans DN42, dont
le LG est le premier. D'où un tier : UNE adresse anycast sur les quatre nœuds, un
Caddy qui distingue les services par leur nom, et des backends où qu'ils soient.

`lg.thystips.dn42` est un CNAME vers `ingress.thystips.dn42`. Un service coûte
donc un nom dans topology/services.yml et RIEN dans le /27 — une adresse par
service aurait fait de ces 32 adresses la limite du nombre de services que l'AS
peut publier. C'est la dernière que ce sujet consomme.

Le franchissement de VRF est la seule partie non évidente, et il est résolu par
systemd plutôt que par Caddy. Une socket de la table globale ne peut pas se lier
à une adresse portée par `lo-dn42` : mesuré, `EADDRNOTAVAIL` en v4 comme en v6.
Avec `SO_BINDTODEVICE` posé avant le bind, mesuré aussi, ça passe dans les deux.
C'est ce que fait `BindToDevice=` dans une unité `.socket` — le descripteur naît
DANS la VRF et est passé à un Caddy qui reste en table globale, d'où il joint les
backends du plan de management sans rien de particulier. C'est le miroir exact du
relais whois du rôle looking_glass, dans l'autre sens.

`FreeBind=true` n'est pas une précaution : l'adresse n'existe pas encore quand la
socket est créée, puisqu'elle est posée par le service — exprès, pour que sa
présence soit le signal de vie que Bird annonce. Et l'ordre des `ListenStream=`
fait partie du contrat, systemd passant les descripteurs dans l'ordre déclaré.

La boucle nftables des services anycast ouvrait `53` EN DUR. Le premier service
qui n'est pas du DNS aurait été annoncé et injoignable, avec en prime du DNS
ouvert sur une adresse qui ne le sert pas. Le port et les protocoles viennent
désormais du service, et l'inventaire refuse un service anycast qui les omet.

Deux pièges désamorcés, tous deux de la famille « ça réussit et c'est faux » :

- la sonde DNS parcourait TOUS les services anycast et les interrogeait en `kdig
  SOA`. La VIP aurait fait partir `Dn42AnycastNotAnswering` en CRITICAL sur les
  quatre nœuds, en permanence, pour un service en parfaite santé. Le filtre porte
  sur le port et non sur le nom, sinon le service suivant y retombait.
- le handler des collecteurs textfile n'avait pas le garde `when:` que portent
  les tâches qui les posent : il redémarrait un timer jamais installé. Dormant
  depuis toujours — il fallait un nœud sans `external-view` ET un gabarit de
  collecteur qui change. Ajouter un collecteur réunit exactement les deux.

Le pare-feu des frontaux n'ouvrait 5000 qu'au reverse proxy clearnet, et l'oubli
s'est vu d'une façon qui vaut d'être notée : à Roubaix l'ingress joint SON PROPRE
frontal, donc par `lo`, que la règle 3 accepte déjà — ça marchait. À Bordeaux il
joint un frontal distant et prenait un 503. Le cas local et le cas distant ne
sont pas le même test ; seule la sonde HTTP a fait la différence.

Les amonts sont classés par proximité et non par ordre alphabétique : avec
`lb_policy first`, l'ordre EST la politique, et un tri par nom faisait préférer
le même amont aux quatre nœuds.

La dérivation `lan.private.v4 -> mgmt.v4`, recopiée dans quatre gabarits faute de
pouvoir passer par `hostvars` pour un default de rôle, devient `dn42_mgmt_address`
dans l'inventaire. C'est ce qui permet enfin à un nœud de nommer l'adresse d'un
autre, et le pare-feu du LG s'en sert aussitôt.

Reporté sciemment : le TLS via l'ACME de burble, et avec lui le magasin de
certificats partagé, le build xcaddy et le flux QUIC sur l'overlay — à l'étape 1
il n'y a rien à partager, et déployer un magasin répliqué inutilisé n'aurait
ajouté que du risque. `discovery: dns` et `plane: dn42` sont dans le schéma et
REFUSÉS par l'inventaire tant que le proxy n'a pas de chemin vers la VRF : un
refus au lint plutôt qu'un service qui répond 502.

Vérifié sur les quatre nœuds : `ss` montre `172.20.187.251%dn42:80`, la VIP suit
le service (arrêt de Caddy sur fr-rbx1 -> retirée -> servi par fr-rbx2 -> retour),
le frontal LG local arrêté bascule sur l'autre amont, la requête depuis la table
globale expire et un port voisin depuis la VRF est fermé. Rendu nftables
identique pour les deux services DNS. Lint vert, playbook idempotent, NetBox
synchronisé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Première brique de l'étape 2. Elle est isolable et se teste seule, ce qui est
la raison de commencer par elle : le reste — build, émetteur, TLS — dépend
d'elle et pas l'inverse.

Le certificat sera un JOKER, et ça change la forme de tout ce fichier. Mesuré
contre le staging de burble : la CA accepte un ordre pour `*.thystips.dn42` et
ne demande QU'UN enregistrement, `_acme-challenge.thystips.dn42`. Le plan
prévoyait une délégation par service — le challenge d'un certificat pour
`lg.thystips.dn42` s'écrivant sur `_acme-challenge.lg.thystips.dn42`, plus
profond qu'une délégation unique ne couvre. Un joker ramène tout à un seul nom,
donc une seule zone. Le prix, dit une fois : il couvre l'ensemble du domaine,
noms de nœuds compris, donc sa clé fuit et c'est tout qui est usurpable.

Déléguée parce que nos deux Knot sont des masters INDÉPENDANTS : un
enregistrement poussé sur l'un ne serait pas servi par l'autre, et le validateur
de la CA interroge celui qu'il veut. Nommer un seul serveur supprime la question.
Point de défaillance unique assumé — s'il tombe, le renouvellement échoue mais
les services continuent d'être servis.

Non signée : le parent prouve la délégation insécurisée par NSEC3, ce qui suffit
à un validateur ACME. Signer une zone dont les enregistrements vivent deux
minutes n'ajouterait qu'une clé à faire tourner.

Deux choses que Knot a refusées, et qu'il valait mieux apprendre au lint :

- `serial-modulo` n'est possible qu'avec `dnssec-signing`. Hérité du gabarit
  `default`, il faisait échouer la validation. Il n'a de toute façon rien à faire
  ici : il existe parce que la zone principale a deux masters aux séries
  indépendantes, et celle-ci n'en a qu'un — ce qui est la raison de la déléguer.
- la directive `key` de knsupdate joint l'algorithme et le nom par `:`, le secret
  suivant après une espace.

`render-check.py` refusait le gabarit parce que SON bouchon du filtre `comment`
n'acceptait pas `decoration`, que le vrai filtre Ansible accepte. Un bouchon plus
étroit que ce qu'il imite transforme un gabarit valide en échec de lint — la même
espèce de mensonge qu'un bouchon plus large, dans l'autre sens.

Vérifié : la mise à jour signée en TSIG est acceptée (`rc=0`) et le serveur
délégué sert l'enregistrement. La délégation est bien dans la zone parente.
NON vérifié encore : la résolution À TRAVERS la délégation, qui est ce que fera
le validateur — le résolveur répond NXDOMAIN, ce qui ressemble à une réponse
négative mise en cache avant que la délégation n'existe, mais ça reste à
confirmer cache vidé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
La release officielle de Caddy n'embarque ni `caddy.storage.swytch` ni
`dns.providers.rfc2136`, les deux modules dont l'étape TLS a besoin. Le binaire
vient donc du service de build du projet Caddy — même origine que la release,
liste de modules qui est la nôtre — plutôt que d'un `xcaddy` local : le go.mod
de Swytch exige Go 1.26, et aucune chaîne Go n'existe sur la machine de contrôle.

La somme est épinglée. Deux requêtes successives rendent le même binaire octet
pour octet, et une reconstruction en amont ferait échouer `get_url` au lieu de
déployer autre chose. Le répertoire d'installation nomme les TROIS versions :
monter un module sans monter Caddy mettrait sinon deux binaires différents au
même endroit.

Et parce qu'une URL demande des modules sans prouver qu'ils sont là, le rôle
vérifie `caddy list-modules` après installation — un magasin de certificats
absent ne se manifesterait autrement qu'au démarrage, dans le journal.

Rien n'est câblé : le tier sert toujours en HTTP clair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
L'ACME de burble, contre son STAGING, avec le magasin partagé qui garantit
qu'un seul nœud émet. Vérifié : une émission sur fr-rbx1, zéro sur les trois
autres, et le même certificat servi par les quatre.

Le certificat est obtenu sur une écoute de boucle plutôt que sur la VIP, et
c'est délibéré : les deux moitiés du problème se vérifient séparément, et une
erreur d'émission ne peut pas rendre les services injoignables. Le port 80
reste le seul que la VIP ouvre.

Le franchissement de VRF n'emprunte pas le même chemin pour les deux besoins,
et c'est la mesure qui l'a décidé. L'AC est distante : par la traversée, le
paquet sort du tunnel avec la source 10.42.255.1, que DN42 ne route pas — donc
un relais sous `ip vrf exec`, et le nom détourné vers lui par un /etc/hosts
monté dans la seule unité caddy. Notre Knot, lui, est local à la VRF du nœud :
la traversée le porte, en UDP comme en TCP.

Un second relais avait été écrit pour ce Knot, puis retiré. `libdns/rfc2136`
force `Net: "tcp"`, donc il portait la mise à jour — mais certmagic vérifie
ensuite la PROPAGATION en interrogeant les serveurs autoritatifs en direct et
en UDP, hors du relais, et le garde-fou d'`output` a fait son travail. D'où la
route de la traversée et l'unique exception d'`output` du jeu de règles : une
destination, un port, une interface.

Le cluster du magasin passe par le plan de management, pas par l'overlay, pour
la même raison que l'AC : Caddy est en table globale et QUIC est de l'UDP
qu'aucun relais systemd ne sait porter. Mesuré dans les deux sens entre
Roubaix et Bordeaux avant d'être écrit.

Trois comportements du module sont documentés dans le README plutôt que
découverts plus tard : la partition bloque la LECTURE autant que l'écriture,
un nœud qui démarre partitionné ne réessaie pas, et des nœuds qui démarrent en
solo se volent le verrou indéfiniment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
La VIP ouvre 80 et 443. Le clair n'est PAS redirigé : la chaîne repose sur la
racine DN42, qu'un pair n'a aucune obligation d'avoir installée, et rediriger
ferait d'un certificat qu'il ne peut pas valider la seule façon de nous joindre.

Côté TLS, UN site `*.thystips.dn42` aiguillé par nom d'hôte donc par SNI. Un
site par service aurait fait demander un certificat par nom, soit une émission
de plus à chaque service publié — exactement ce que le générique évite. Les
amonts vivent dans un extrait importé par les deux schémas.

`port` devient `ports`, une liste, parce qu'une adresse anycast en sert
désormais deux. Un scalaire aurait imposé soit une seconde adresse, soit une
règle de pare-feu écrite à la main à côté de celle qui est générée.

L'alerte d'expiration ne vient pas de Caddy : CADDY NE PUBLIE AUCUNE MÉTRIQUE
DE CERTIFICAT — son /metrics expose les rechargements, la santé des amonts et
le runtime Go, rien d'autre. D'où un collecteur qui mesure ce qui est SERVI,
poignée de main comprise, ce qui est la seule façon de distinguer « personne
n'a renouvelé » de « ce nœud sert encore l'ancien ». Il publie aussi l'émetteur
en label et la validation contre la racine DN42 : c'est ce qui attraperait
l'erreur du collecteur de routes de DN42, servir une chaîne de staging en
production sans s'en apercevoir.

Deux pannes rencontrées et corrigées en chemin. HTTP/3 refuse de démarrer sur
un descripteur de flux — « network 'fd' cannot handle HTTP/3 connections » —
et est donc coupé explicitement ; invisible tant qu'on servait en clair. Et le
rechargement passe désormais EN DERNIER dans les handlers : le passage au TLS
change la socket et le Caddyfile dans la même exécution, et le rechargement
partait avec les anciens descripteurs.

Enfin, le magasin partagé ne persiste RIEN sur disque. Un redémarrage roulant
coince le cluster sur des identifiants de nœuds morts et plus personne ne sert
de certificat, pendant que le clair continue — donc panne partielle et
silencieuse. Le remède est un arrêt complet puis un démarrage complet, et il
est dans le README avec le reste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le gabarit et la matrice se modifient dans le même commit, et celui du TLS a
oublié la moitié documentaire de la généralisation `port` → `ports`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
La bascule sur `/v1/dn42/acme/directory` était attendue par le plan derrière
l'observation d'un renouvellement complet en staging. C'est le staging lui-même
qui a invalidé cette précaution : sa chaîne remonte à `CN=ca.acme.burble.dn42`,
une racine auto-signée hors PKI DN42, donc `chain_trusted` y reste à 0 par
construction — un renouvellement observé là n'aurait pas exercé la vérification
qui compte. S'y ajoute que l'AC de burble n'implémente aucune limite de taux.

Le certificat servi valide désormais contre `dn42-root-ca.crt`.

Au passage, un piège mesuré : les deux hiérarchies de burble portent le MÊME
sujet d'intermédiaire, `CN=burble.dn42 staging ACME`, et ne diffèrent que par la
clé. Un certificat de production affiche donc « staging ». Trois conséquences
appliquées ici : la règle Mimir perd son exception sur le nom de l'émetteur, qui
l'aurait tue pour toujours en production ; `dn42_ingress_cert_issuer` est
requalifiée en série de diagnostic ; et le collecteur de routes de DN42, que la
doc accusait de servir une chaîne de staging, est très probablement hors de cause.

Swytch est replié en stockage local derrière `ingress_shared_storage`, après une
panne franche — cluster non reformé au redémarrage, `lock lost` en boucle, aucun
certificat émis sur les quatre nœuds. Le drapeau vit dans `group_vars/all/` parce
que `nftables` en dépend aussi : le flux 7380/udp de la ligne 18 n'a de raison
d'être que si le magasin est actif, et il est donc fermé.

Ce que le repli ne rattrape pas, et qui décide de la suite : un seul nœud sait
faire le challenge `dns-01`, la zone déléguée vivant sur la loopback DN42 de
fr-rbx1 que la traversée ne porte que pour lui. Le magasin partagé n'économise
pas des émissions, il est la seule façon dont trois nœuds sur quatre peuvent
avoir un certificat. La cible est un cluster OpenBao sur les nœuds eux-mêmes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le magasin partagé que le tier d'ingress attend, en cluster raft sur le plan de
management et descellé par le moteur transit de ca1. Un seul nœud sait résoudre
le challenge dns-01 : ce coffre est ce qui permet aux trois autres d'avoir un
certificat, pas une économie d'émissions.

Le rôle laisse dehors, comme celui d'ATNET laisse `bao operator init` : le
contenu du coffre, l'initialisation, et le certificat du listener — il faudrait
pour l'émettre un token capable de signer n'importe quel nom sous
thystips.cloud, plus dangereux que celui du sceau. Le rôle refuse de configurer
un listener sans lui plutôt que de le laisser échouer au démarrage.

Trois faits mesurés contre le binaire 2.6.1 plutôt que repris des exemples
Vault qui circulent :

  - `disable_mlock` n'existe plus, OpenBao a retiré mlock et le champ est
    rejeté. `MemorySwapMax=0` dans l'unité devient la seule barrière entre les
    secrets et le swap, ce que la doc de Vault laisserait croire redondant.
  - `bao operator diagnose` rend 4 sur une configuration valide COMME sur du
    HCL cassé : inutilisable en `validate:`, où Ansible exige 0. Le filet
    reste render-check et un redémarrage qui échoue franchement.
  - les quatre nœuds joignent ca1 en 200 sur sys/health, Bordeaux compris, et
    leur résolveur interne rend l'adresse privée. Le sceau ne demande donc
    aucun flux nouveau ni AC épinglée.

Ligne 19 de la matrice : 8200 et 8201 en TCP, entre nos seules adresses de
management. Les deux ports vont ensemble — ouvrir l'API sans le port de
cluster donne quatre coffres qui se voient et ne répliquent rien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le contrôle du certificat de listener était placé après l'utilisateur, les
répertoires et le binaire. Déroulé sur fr-rbx1, ça donne deux erreurs pour une
cause : l'assertion, puis le handler `Restart openbao` notifié par les tâches
déjà passées, qui échoue sur une unité pas encore installée — et c'est la
seconde qu'on lit en premier. Un nœud qu'on ne peut pas finir ne se provisionne
pas à moitié.

Les DROITS du certificat deviennent en revanche l'affaire du rôle, même si son
contenu vient d'ailleurs : c'est lui qui décide de ce que le démon a le droit de
lire. Sans quoi un certificat déposé à la main en root:root est illisible par le
service, et l'échec arrive au démarrage plutôt qu'au moment du dépôt.

Deux faits de mise en service dans le README : `sys/health` rend 429 sur un nœud
standby descellé — une sonde qui compte tout ce qui n'est pas 200 signalerait
trois nœuds sur quatre — et un 503 sur `raft/bootstrap/challenge` pendant le
premier démarrage est le déroulement normal, OpenBao interrogeant les quatre
adresses en parallèle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
thystips deleted branch feat/dn42-ingress-tier 2026-08-07 21:48:29 +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!12
No description provided.