feat(openbao) : le magasin de certificats du tier d'ingress #13

Merged
thystips merged 5 commits from feat/dn42-ingress-tier into main 2026-08-07 23:11:04 +02:00
Owner

Les trois nœuds qui ne savent pas résoudre le challenge dns-01 servent enfin en TLS. Un cluster OpenBao à quatre nœuds remplace caddy.storage.swytch, tombé en panne franche : il persiste sur disque, se réplique par raft et se descelle seul auprès du moteur transit de ca1.

  • openbao_config — mount KV v2, politique de stockage, AppRole. Playbook séparé : il demande un token d'administration qui ne vit pas dans ce dépôt.
  • openbao_agent — le bao agent qui renouvelle le token que Caddy surveille, sous un compte dédié pour qu'un Caddy compromis rende un token d'une heure et non un accès permanent.
  • ingress — binaire reconstruit avec caddy.storage.vault, Swytch et son flux 7380 retirés, ligne 18 de la matrice réécrite.

Mesuré après bascule : une émission sur fr-rbx1, zéro sur les trois autres, les quatre servant le même certificat avec Verify return code: 0 (ok). Silence Alertmanager levé.

Six pièges mesurés plutôt que supposés sont documentés dans les READMEs des rôles — le plus vicieux étant qu'un bloc bao { } dans la configuration d'un bao agent parse sans erreur et est ignoré, l'agent retombant sur 127.0.0.1.

Les trois nœuds qui ne savent pas résoudre le challenge dns-01 servent enfin en TLS. Un cluster OpenBao à quatre nœuds remplace caddy.storage.swytch, tombé en panne franche : il persiste sur disque, se réplique par raft et se descelle seul auprès du moteur transit de ca1. - openbao_config — mount KV v2, politique de stockage, AppRole. Playbook séparé : il demande un token d'administration qui ne vit pas dans ce dépôt. - openbao_agent — le bao agent qui renouvelle le token que Caddy surveille, sous un compte dédié pour qu'un Caddy compromis rende un token d'une heure et non un accès permanent. - ingress — binaire reconstruit avec caddy.storage.vault, Swytch et son flux 7380 retirés, ligne 18 de la matrice réécrite. Mesuré après bascule : une émission sur fr-rbx1, zéro sur les trois autres, les quatre servant le même certificat avec Verify return code: 0 (ok). Silence Alertmanager levé. Six pièges mesurés plutôt que supposés sont documentés dans les READMEs des rôles — le plus vicieux étant qu'un bloc bao { } dans la configuration d'un bao agent parse sans erreur et est ignoré, l'agent retombant sur 127.0.0.1.
Le contenu du coffre — mount KV v2, politique de stockage, AppRole — dans un
rôle et un playbook SÉPARÉS du rôle `openbao`. Celui-ci configure un démon sur
chaque nœud et se joue à chaque déploiement ; celui-là configure un coffre, une
fois pour les quatre, et demande un token d'administration qui ne vit pas dans
ce dépôt. Les fondre ferait échouer tout déploiement ordinaire faute de token.

La politique n'est pas un choix : le module de stockage vérifie ses propres
capacités au démarrage (`CheckCapabilities()` interroge `sys/capabilities-self`)
et refuse de se provisionner si l'une manque. Les trois chemins et leurs
capacités sont donc lus dans `storage.go`, pas déduits — et ce qui n'y est pas
compte autant : ni `destroy`, ni `undelete`, ni rien hors du préfixe.

Le rôle REFUSE un mount KV v1 du même nom plutôt que d'essayer de le corriger :
le module pose un `cas` sur chaque écriture de verrou, que seul KV v2 comprend.
Sur un v1 le verrou d'émission ne verrouillerait rien, et ça ne se verrait que
le jour où deux nœuds émettent en même temps.

Le `secret_id` ne s'expire pas, faute d'orchestrateur pour en livrer un nouveau
— un secret_id expiré, c'est un Caddy qui ne relit plus ses certificats après un
redémarrage. Ce qui borne le risque à la place est explicite : les deux liaisons
CIDR, dérivées du groupe comme les pairs raft.

Le lint vérifiait la syntaxe du seul `playbook.yml`. Il les vérifie tous
maintenant — ceux qui se jouent délibérément sont précisément ceux qu'on ne
déroule pas assez souvent pour qu'une faute s'y voie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Les deux listes de CIDR partent identiques et ne reviennent pas pareil :

    secret_id_bound_cidrs : "10.2.242.202/32"   ← le /32 conservé
    token_bound_cidrs     : "10.2.242.202"      ← le /32 retiré

`token_bound_cidrs` traverse le parseur d'adresses de sockaddr, qui réduit un
/32 à sa forme nue ; `secret_id_bound_cidrs` est stocké tel quel. Le test de
sous-ensemble par `combine()` y voyait une dérive éternelle sur une valeur
pourtant juste.

La comparaison retire le /32 des DEUX côtés plutôt que d'envoyer d'emblée une
forme nue en pariant sur le comportement du parseur : on compare ce qu'on veut
dire, pas ce que la bibliothèque en a fait.

La leçon dépasse OpenBao et vaut d'être écrite : `combine()` en test de
sous-ensemble suppose que l'API rend ce qu'on lui donne, et un champ normalisé à
l'écriture casse cette hypothèse dans le sens le plus bénin — du bruit permanent,
qui finit par faire ignorer un vrai changement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le module de stockage ne sait pas s'authentifier — il SURVEILLE un fichier de
token par fsnotify, ce qui fait d'un token changeant le mode de fonctionnement
prévu plutôt qu'un contournement. Un token vaulté aurait été soit éternel, soit
une panne à date fixe.

L'agent tourne sous SON PROPRE compte, et c'est le cœur du montage : un Caddy
compromis rend un token d'une heure, pas le couple (role_id, secret_id) qui vaut
un accès permanent. La jonction passe par le bit SETGID du répertoire de sortie
— le sink d'OpenBao sait poser un mode, pas un propriétaire, et sans ce bit la
seule sortie aurait été de faire tourner l'agent sous `caddy`, c'est-à-dire
d'abandonner la séparation qu'on venait de construire.

Deux pièges mesurés sur fr-rbx1 plutôt que supposés :

  - LE BLOC S'APPELLE `vault`, PAS `bao`. Sur un produit qui a renommé sa CLI en
    `bao` et ses variables en BAO_*, un bloc `bao { address = … }` parse sans
    erreur et est IGNORÉ : l'agent retombe sur 127.0.0.1:8200, où rien n'écoute.
    Vérifié en pointant les deux variantes vers une adresse volontairement
    fausse — `vault` la reprend, `bao` ne la voit jamais.
  - `remove_secret_id_file_after_reading` vaut `true` PAR DÉFAUT. L'agent
    supprime son secret_id après lecture ; tout marche jusqu'au premier
    redémarrage de l'unité, où Caddy perd son magasin. Une panne différée que
    déclenche le geste le plus banal qui soit.

Le rôle ne réclame un token d'administration que si le secret_id manque, donc au
premier passage sur un nœud et jamais ensuite — c'est ce qui lui permet de vivre
dans le playbook principal. Et il ATTEND que le fichier de token apparaisse : un
agent qui échoue en boucle s'affiche `active (running)`, et rien d'autre ne le
dirait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Binaire reconstruit avec `caddy.storage.vault` (gerolf-vent/caddy-vault-storage
v1.2.3) à la place de `caddy.storage.swytch`. Somme identique sur deux
téléchargements, modules vérifiés dans le binaire : plus aucune trace de Swytch.

Ce que le nouveau module fait différemment, et qui a décidé du choix : il ne
stocke rien lui-même. Il parle à un coffre qui persiste, réplique et se
sauvegarde, et il accepte une LISTE d'adresses avec bascule — la disponibilité
vient du coffre plutôt que d'un protocole de cluster embarqué dans un serveur
web, ce qui est exactement ce qui avait lâché.

Trois pièges de son parseur, lus dans `module.go` :

  - `addresses` prend UNE chaîne séparée par des virgules. `addresses "a" "b"`
    n'est pas une erreur : le second argument est lu comme une clé, et le switch
    n'ayant pas de branche par défaut, il est ignoré en silence. Une option mal
    orthographiée disparaît pareillement.
  - `lock_timeout` et `lock_check_interval` sont des entiers nus en secondes.
    `60s` fait échouer strconv.Atoi et arrête le chargement sur un message qui
    ne nomme pas la clé.
  - le vrai filet est son contrôle de capacités au démarrage, qui rend un mount
    ou un préfixe désaccordé de la politique franchement fatal.

`ingress_shared_storage` REVIENT dans les defaults du rôle. Il vivait dans
group_vars/all/ parce que nftables en dépendait — le flux QUIC 7380 n'avait de
raison d'être que si le magasin était actif. Ce flux disparaît avec Swytch : la
ligne 18 de la matrice est retirée, celle du coffre prend son numéro, et un
drapeau documentant une dépendance morte est pire qu'inutile.

Retirés au passage : la passphrase Swytch du vault, le nom de cluster dans le
/etc/hosts privé, et `nftables_ingress_cluster_port`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Trois défauts trouvés en déroulant, dont le premier empêchait Caddy de démarrer.

L'AC D'ATNET PAR L'ENVIRONNEMENT. `caddy.storage.vault` n'expose aucune option
`ca_cert` : il construit son client avec `vault.DefaultConfig()`, qui lit
l'environnement. Sans `VAULT_CACERT`, Caddy refuse de démarrer — « all servers
are unavailable », précédé d'un `x509: certificate signed by unknown authority`
par adresse. Et c'est `VAULT_CACERT`, pas `BAO_CACERT` : le module embarque
github.com/hashicorp/vault/api, et c'est la bibliothèque qui nomme la variable.

Que l'échec soit FRANC est la bonne nouvelle de cette panne : le module vérifie
la santé des quatre coffres à son provisionnement, donc une AC absente tombe au
démarrage et non à la première écriture, un mois plus tard.

LE COFFRE LOCAL EN PREMIER. Le module retient « the first healthy instance »
dans l'ordre de la liste. Triée par nom d'hôte, `fr-bod1` arrivait en tête : les
QUATRE nœuds allaient chercher leur magasin à Bordeaux — mesuré, `Connected
{"address": "https://10.2.242.202:8200"}` depuis Roubaix.

UN NIVEAU DE JOURNAL. `ingress_log_level`, INFO par défaut. Le module raconte
chaque Stat, Load et Store en DEBUG et à aucun autre niveau ; sans lui, une
panne de magasin ne montre que son verdict, et « storage is probably
misconfigured » ne dit pas laquelle des trois opérations a échoué.

MESURÉ APRÈS BASCULE, et c'est la propriété que tout ce chantier achetait :
UNE émission sur fr-rbx1, ZÉRO sur les trois autres, les quatre servant le même
certificat (notAfter identique à la seconde) avec `Verify return code: 0 (ok)`
contre la racine DN42. Les trois nœuds qui ne savent pas résoudre `dns-01` ont
enfin un certificat.

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 23:11:04 +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!13
No description provided.