l edge servait du snakeoil, et deux ecritures d une meme regle

Le certificat bouchon de Debian sur les trois noms. serveur_nginx l a pour
defaut, avec un commentaire d avant la PKI ; un locataire le remplace en
group_vars, l inventaire du site est dynamique et n en a pas. Les autres replis
rendaient un service muet — celui-la rend du TLS qui RESSEMBLE a du TLS : le
cadenas s affiche et rien n est prouve.

site_inventaire construisait sa propre liste d expositions, avec edge = le
groupe de l application. Vrai tant que le site n avait pas d edge. client_pki
retient un FQDN si son edge est un groupe de cet hote : aucun ne correspondait,
et le certificat ne portait que le nom de la machine. La boucle est remplacee
par un appel a expositions_des_applications.

Et la cicatrice de site-forge-01 — un cert renouvele n atteint personne tant que
son consommateur n est pas recharge — a ete refaite sur la machine suivante.

Les 6 noms sont maintenant servis et verifies contre l AC du site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-14 21:44:03 -04:00
parent 4f68182ac6
commit fae3cc8838
2 changed files with 107 additions and 9 deletions

View file

@ -1,5 +1,70 @@
# CHANGELOG — Set-OPS
## 2026-09-14 (23) — Du TLS qui ressemblait a du TLS, et une regle qu'un tenant n'a jamais eu besoin d'ecrire
Trois defauts de plus sur le chemin de l'edge, et le premier est le plus grave de la
journee.
### L'edge servait `ssl-cert-snakeoil.pem`
Le certificat bouchon de Debian, sur les trois noms. `serveur_nginx` l'a pour DEFAUT,
avec un commentaire qui date d'avant la PKI : *« a remplacer par des certificats de l'AC
interne plus tard »*. Un locataire le remplace dans `group_vars/serveur_nginx.yml` ;
l'inventaire du site est DYNAMIQUE et n'a pas de group_vars.
LES AUTRES REPLIS RENDAIENT UN SERVICE MUET OU UNE REGLE INERTE. Celui-la rend **du TLS
qui ressemble a du TLS** : le cadenas s'affiche, la connexion est chiffree, et rien n'est
prouve — le certificat n'est signe par personne et ne porte aucun des noms servis. C'est
la seule sorte de panne qui rassure.
### Deux ecritures d'une meme regle, et seule l'une a appris
`site_inventaire.py` construisait sa PROPRE liste d'expositions, avec `edge` = le groupe
de l'application. Le commentaire disait pourquoi : *« le site n'a pas d'edge, donc chaque
service se sert lui-meme »*. C'etait vrai jusqu'a hier.
`client_pki` retient un FQDN si son `edge` est un GROUPE DE CET HOTE. L'edge appartient a
`serveur_nginx` ; cette boucle rendait `serveur_grafana`, `serveur_icingaweb2`... Aucun
nom ne correspondait. La boucle est remplacee par un appel a
`expositions_des_applications` — la regle n'existe plus qu'une fois.
### La cicatrice refaite sur la machine suivante
`site-forge-01` porte ce commentaire depuis des semaines : *« Un cert renouvele sur disque
reste servi perime tant que le consommateur n'est pas recharge. La cicatrice est deja dans
le depot ; on ne la refait pas. »* Elle a ete refaite sur `site-edge-01`, qui n'avait pas
de `client_pki_reload_services`.
disque : forge, pki, dns, sauvegarde, observatoire, vigie, site-edge-01
servi : site-edge-01
### Ou en est l'edge
certificat servi les 6 noms, signe par l'AC du site
verification TLS verif_tls=0 depuis le poste, contre la racine du site
vhosts forge, observatoire, vigie — en 443
### CE QUI RESTE, ET C'EST UNE VRAIE PIECE
Les trois noms repondent en TLS **verifie**, puis rien : `http=000`. L'edge ne joint pas
ses amonts, et le devis le dit lui-meme :
note : serveur_nginx declare un port `derive` que le plan du site ne resout pas
— aucune regle emise.
`serveur_nginx` declare bien `egress port: derive, pair: expositions`. Ce flux n'a JAMAIS
eu besoin d'etre resolu a la frontiere : chez un locataire, les passerelles de zone sont
tenues par le SDN de Proxmox, et le trafic edge -> amont ne traverse pas le boitier. **Au
site, elles sont tenues par la frontiere** — et c'est le premier endroit ou un edge doit
franchir la bordure pour atteindre ce qu'il relaie.
Resoudre `expositions` en une regle PAR AMONT (son hote, son port) est la piece qui
manque. Elle se voit maintenant parce que le site est la premiere fabric ou un edge et
ses services vivent dans des zones differentes.
Et `forge` relaie en `http://10.37.33.11:443` — du clair vers un port TLS, d'ou son `400`.
L'amont d'une exposition deja servie en TLS doit etre `https://`.
## 2026-09-14 (22) — L'edge du site est debout, et quatre replis silencieux l'ont retarde
`site-edge-01` est nee, deployee, et sert `forge`, `observatoire` et `vigie` en 443. Le

View file

@ -31,6 +31,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import yaml # noqa: E402
import underlay as U # noqa: E402
from inventory_rules import expositions_des_applications # noqa: E402
from inventory_rules import integrations_universelles # noqa: E402
# L'inventaire des tenants passe par ce compte ; le site n'a aucune raison d'en differer.
@ -386,15 +387,28 @@ def inventaire() -> dict:
# LES EXPOSITIONS : le nom du SERVICE, pas celui de la machine. Un nom de service
# survit au demenagement du service ; un nom de machine, non.
#
# `edge` nomme le GROUPE qui sert ce FQDN — c'est ce qu'attend `hosts_statiques`.
# Chez un tenant c'est l'edge nginx, qui termine le TLS pour tout le monde. Le site
# n'a pas d'edge : chaque service se sert lui-meme, donc le groupe est le sien. La
# forme reste la meme, ce qui la remplit change.
expositions: list[dict] = []
for nom_app, app in applications.items():
for fqdn in (app.get("expose") or []):
expositions.append({"fqdn": str(fqdn), "edge": str(app.get("groupe") or ""),
"application": nom_app})
# `edge` nomme le GROUPE qui sert ce FQDN — c'est ce qu'attend `hosts_statiques`, et
# c'est de la que `client_pki` tire les SAN du certificat.
#
# ON APPELLE LA FONCTION PARTAGEE, ON NE LA REECRIT PLUS (2026-09-14).
#
# Cette boucle disait : « le site n'a pas d'edge, donc chaque service se sert
# lui-meme, donc le groupe est le sien ». C'etait vrai — jusqu'au jour ou le site a
# eu un edge. La meme regle vivait alors a DEUX endroits : ici, et dans
# `expositions_des_applications`, qui lit le registre des domaines. Seule la seconde
# a appris ; celle-ci est restee sur l'ancienne verite.
#
# CE QUE CA A COUTE. `client_pki` retient un FQDN si son `edge` est un GROUPE DE
# CET HOTE. L'edge appartient a `serveur_nginx` ; cette boucle rendait
# `serveur_grafana`, `serveur_icingaweb2`... Aucun nom ne correspondait, et le
# certificat de `site-edge-01` ne portait QUE celui de la machine — du TLS correct
# sur trois noms que personne ne peut appeler.
#
# Deux ecritures d'une meme regle finissent par diverger. Celle-ci n'existe plus.
expositions = expositions_des_applications(
{"applications": applications},
{"domaines_publics": (U.lire_plan_site("domaines.yml") or {}).get("domaines_publics") or {}},
)
# --- LES GARDES DU PLAN, LA OU LE PLAN EST LU ----------------------------
#
@ -666,6 +680,25 @@ def inventaire() -> dict:
# un. Les deux autres modes d'Icinga Web 2 (`ldap`, `external`) supposent
# l'un ou l'autre ; ici, nginx authentifie et l'application le croit.
"serveur_icingaweb2_auth": "locale",
# L'EDGE SERT LE CERTIFICAT DE L'AC INTERNE, PAS LE BOUCHON DE DEBIAN.
#
# `serveur_nginx` a pour DEFAUT `ssl-cert-snakeoil.pem`, avec un commentaire
# qui date d'avant la PKI : « a remplacer par des certificats de l'AC interne
# plus tard ». Chez un locataire, `group_vars/serveur_nginx.yml` le remplace.
# L'inventaire du site est DYNAMIQUE et n'a pas de group_vars : l'edge est
# donc ne en servant du snakeoil.
#
# C'EST LE PLUS GRAVE DES REPLIS DE LA JOURNEE. Les autres rendaient un
# service muet ou une regle inerte — celui-la rend du TLS qui RESSEMBLE a du
# TLS. Le cadenas s'affiche, la connexion est chiffree, et rien n'est prouve :
# le certificat servi n'est signe par personne et ne porte aucun des noms.
#
# Le chemin est celui que `client_pki` ecrit, et le meme que celui des
# locataires — une seule convention, pas deux.
"serveur_nginx_certificat":
"/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.crt",
"serveur_nginx_cle":
"/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key",
# Le depot du site, pour les machines du site qui y deposent. Vide s'il n'y
# en a pas — `client_backup` refuse alors, plutot que de viser un nom qui
# ne repond pas : mieux vaut un deploiement qui s'arrete qu'une sauvegarde