Set-OPS-Public/docs/conception/mecanismes.md

78 lines
10 KiB
Markdown
Raw Normal View History

# Les mécanismes transverses
> **Pour qui :** le **mainteneur** — celui qui va *modifier* le moteur. À lire avant
> d'ajouter quoi que ce soit : ces mécanismes vivent dans le code et les README de rôles, et
> on les re-découvre sinon. Le survol est dans `docs/architecture.md` ; l'index des
> documents dans `docs/README.md`.
## Les mécanismes transverses, et où ils vivent
Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanisme.**
| Mécanisme | Ce que c'est | Où, dans le code | Doc |
|---|---|---|---|
| Plan → inventaire | `plan/*.yml` → `hosts.yml` généré | `scripts/instancier.py`, `scripts/inventory_rules.py` | `plan-et-generation.md` |
schema du plan : la forme des registres devient derivee, et gardee Etape 2 du chantier « l UI reflete fidelement la structure ». Le GUI porte CHAMPS_ECRITS_PAR_GUI, une liste tenue A LA MAIN de ce qu il sait ecrire, que P19 confronte au reel. C est une copie — gardee, donc honnete, mais une copie : quelqu un doit penser a l allonger. `make schema` produit docs/audit/schema-plan.json : six registres, 42 champs, leurs types, leurs enumerations et ce qui est requis. CE QUE LE SCHEMA EST, ET CE QU IL N EST PAS schema -> la FORME -> generera les champs du formulaire validateurs -> la COHERENCE -> refusent une saisie incoherente Un JSON Schema ne sait pas dire qu un `consommateur` designe une application inexistante, ni qu une integration universelle recopiee au plan est un defaut. Vouloir l y mettre creerait la seconde source de verite que tout ce depot refuse. Les valider_* restent l autorite. LES ENUMERATIONS SONT IMPORTEES, JAMAIS RECOPIEES ETATS_SERVEUR, PORTEES_BD et AUTORITES_DNS viennent des constantes que les validateurs appliquent. Une enumeration recopiee diverge — c est la lecon des neuf resolutions d instance que P41 garde depuis. CE QUE L ETAPE 1 AVAIT TROUVE, ET QUE P61 A CONFIRME Le recensement montrait `categorie` et `service` presents dans TOUS les plans et absents de CHAMPS_ECRITS_PAR_GUI, dont la ligne `nomenclature` est vide : le GUI ne sait pas les editer, l operateur doit ouvrir le YAML. P61 a refuse le premier schema pour cette raison exacte. Les trois tables imbriquees de la nomenclature sont donc DECRITES et non resumees en « object ». 31 champs observes dans l instance courante, 42 decrits par le schema. La difference n est pas du bavardage : observer une instance n est pas un schema. `noeud`, `stockage` et `coeurs` sont legitimes et simplement inutilises ici — un schema derive de l observation les INTERDIRAIT. P61, EPROUVEE DANS LES DEUX SENS fichier genere perime -> REFUSE champ du plan absent du schema -> REFUSE champ decrit mais inutilise au plan -> COMPTE, pas refuse Le troisieme point est delibere : refuser obligerait a retirer du schema un champ valide des que plus personne ne s en sert. Meme mesure que les lacunes nommees de P29. UN DEFAUT DE MON INSTRUMENT, PAYE EN ROUTE P61 comparait des noms a plat quand couverture_gui aplatit les tables imbriquees : elle criait sur un schema correct. L instrument mesurait autre chose que la cible. On aplatit desormais des deux cotes. make prouver : CONFORME, 60 OK, 0 echec, 1 saute. Prochaine etape : generer les formulaires depuis ce schema, et retirer CHAMPS_ECRITS_PAR_GUI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:19:20 -04:00
| **Schéma des registres** | la **forme** des six registres — champs, types, énumérations, requis — **dérivée** des validateurs, jamais écrite à la main. Sert à générer les formulaires plutôt qu'à les écrire. La **cohérence** reste aux `valider_*` : un schéma ne sait pas dire qu'un `consommateur` désigne une application inexistante | `scripts/schema_plan.py` (`make schema`) → `docs/audit/schema-plan.json` ; garde **P61** | `plan-et-generation.md` |
| Nomenclature dérivée | VMID / IP / VLAN / FQDN dérivés | `inventory_rules.deriver_nomenclature` + `plan/nomenclature.yml` | `nomenclature-vm.md` |
| Dimensionnement | RAM/CPU/disque sommés par logiciel | `roles/*/meta/empreinte.yml` → `deriver_ressources` | `dimensionnement-ressources.md` |
| **Bindings app→app** | lien **côté app** (`liens`) résolu en host_vars | `plan/applications.yml` `liens:` + `roles/*/meta/liens.yml` + `instancier.resoudre_liens` | `bindings-conception.md` |
docs : un README par rôle (12 manquants) + carte remise à l'état du code Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont désormais un README, au format maison (intention → rôle → variables → notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches : - serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à toute la flotte ; le vrai travail est dans le playbook de groupe. - hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de reconstruction (résolution par nom avant que PowerDNS existe) ; fichier entièrement géré, alias d'exposition best-effort. - resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le FQDN plutôt que le nom court (fédération + nom canonique pour verify-full). - serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise (static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine. - serveur_postfix : liens mailstore/milter, recopie de /etc/hosts dans le chroot. - serveur_rspamd : clé DKIM idempotente ; le domaine signé doit être PUBLIC en prod, sinon la signature ne vaut rien pour les MX distants. - client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client, deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ; hors-nœud n'est pas hors-site. - serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi, et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP. - serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO. - client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle avant de toucher /etc/resolv.conf). docs/carte-set-ops.md ne décrivait plus l'état du code : - expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») : expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs d'edge dérivés par instancier.py ; - 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de résolution, resoudre_base nommé dans les bindings app→base) ; - 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement, preuve/recette, exploitation courante, wiki — pans du corpus non indexés ; - ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix, requiert non consommé au déploiement (l'ordre vient des couches + du graphe). make verifier vert : ansible-lint 514 fichiers, tests, syntax-check, 23 preuves CONFORME / 0 échec / 0 sauté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:32:06 -04:00
| **Bindings app→base** | lien **côté base** (`consommateur`/`portee`) résolu **dans le rôle** | `plan/bases-donnees.yml` + rôle utilitaire `resoudre_base` (`lookup('vars', secret)`, `no_log`) inclus par le consommateur | `bindings-conception.md` §5 |
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
| Résolution d'annuaire | connexion LDAP (uri/base DN/bind) **dérivée**, jamais recopiée | rôle utilitaire `resoudre_annuaire` (inclus par dovecot/postfix/keycloak/icingaweb2 et `amorcage_acces`) | `identite-sso.md` |
docs : un README par rôle (12 manquants) + carte remise à l'état du code Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont désormais un README, au format maison (intention → rôle → variables → notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches : - serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à toute la flotte ; le vrai travail est dans le playbook de groupe. - hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de reconstruction (résolution par nom avant que PowerDNS existe) ; fichier entièrement géré, alias d'exposition best-effort. - resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le FQDN plutôt que le nom court (fédération + nom canonique pour verify-full). - serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise (static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine. - serveur_postfix : liens mailstore/milter, recopie de /etc/hosts dans le chroot. - serveur_rspamd : clé DKIM idempotente ; le domaine signé doit être PUBLIC en prod, sinon la signature ne vaut rien pour les MX distants. - client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client, deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ; hors-nœud n'est pas hors-site. - serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi, et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP. - serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO. - client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle avant de toucher /etc/resolv.conf). docs/carte-set-ops.md ne décrivait plus l'état du code : - expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») : expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs d'edge dérivés par instancier.py ; - 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de résolution, resoudre_base nommé dans les bindings app→base) ; - 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement, preuve/recette, exploitation courante, wiki — pans du corpus non indexés ; - ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix, requiert non consommé au déploiement (l'ordre vient des couches + du graphe). make verifier vert : ansible-lint 514 fichiers, tests, syntax-check, 23 preuves CONFORME / 0 échec / 0 sauté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:32:06 -04:00
| Plancher de résolution | `/etc/hosts` généré depuis l'inventaire + alias d'`expose` → l'écosystème se résout **DNS éteint** | rôle `hosts_statiques` (appliqué dans la couche socle) | `dns-interne.md` |
| Pont de certificat | cert step_ca → service, resync au renouvellement | script `*-cert-sync` + unité `.path`, dans chaque rôle serveur ; cert déposé par `client_pki` | — |
| Ordonnancement socle-first | socle/durci avant les `client_*` | `serveur_debian`/`serveur_durci` d'abord (posent `/etc/hosts` via `hosts_statiques`) | — |
| Sûreté check-mode | dry-run fiable | `when: not ansible_check_mode` sur les tâches de service + handlers | — |
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
| Voûte au déploiement | secret jamais en clair ; **une voûte, une clé** depuis le 2026-08-28 | `ANSIBLE_VAULT_IDENTITY_LIST` construit par `scripts/voutes.py` (clé nommée `~/.config/setops-vault-<dépôt>`) ; déréférencé par `lookup('vars', <nom>)` | `autorisation.md` §6.1 |
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
| Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* : hyperviseurs et frontière **supervisés** depuis le 2026-09-17 — températures, ventilateurs, SMART, usure NVMe, un catalogue (`scripts/materiel.py`) lu par le tableau Grafana « Matériel » et le service Icinga `materiel` (`supervision-conception.md`). Les commutateurs restent sans supervision ; aucun équipement n'a de sauvegarde de configuration | `hebergeur-exploitation.md` |
authentification : SSO Keycloak devant, secours par sudo, formulaire local fermé Directive : toute authentification web passe par Keycloak, LDAP est la source unique des comptes, chaque service garde un accès de secours par sudo sur l'hôte. Les trois sont indissociables — la chaîne service → Keycloak → LDAP est en série, donc sans secours une panne exclut tout le monde, y compris pour réparer. Portée : le web seulement ; IMAP/SMTP se lient à LDAP directement et SSH est en clé seule. Posture <rôle>_connexion_locale, false par défaut. Le compte local existe — il ne peut pas dépendre de Keycloak — mais son formulaire n'est plus proposé au repos : ouvert en permanence, il contourne la politique de mot de passe, le MFA et surtout la révocation centrale. Vérifié auprès de l'amont, puis par rendu réel des gabarits dans les deux postures : - Grafana GF_AUTH_DISABLE_LOGIN_FORM → ferme ; - Forgejo ENABLE_INTERNAL_SIGNIN + ENABLE_BASIC_AUTHENTICATION → ferme, API Basic comprise. N'existe que depuis la v10 (ticket amont 7476) ; le rôle épingle 10.0.0 et un assert refuse la fermeture en deçà, car le réglage serait ignoré sans erreur ; - Nextcloud hide_login_form → MASQUE seulement : ?direct=1 reste le chemin de secours documenté par l'amont. Écrit comme tel, sans prétendre à l'équivalence. Défaut attrapé par le rendu : la condition Forgejo sans `| bool` n'émettait rien dans aucune posture — une valeur en chaîne est vraie au sens Jinja, la connexion locale serait restée ouverte en silence. docs/authentification.md, décisions D-38 à D-41. ansible-lint (production) sans échec, 28 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:50:36 -04:00
| Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` |
| Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) |
| **DNS public** | le locataire **écrit** ses zones `autorite: primaire-cache`, le site les **sert** en secondaire, un site pair les réplique. Transfert ouvert **à la clé TSIG seule** (`allow-axfr-ips` et TSIG sont alternatifs dans PowerDNS) ; instance `pdns@public` à part pour que le site ne puisse jamais interroger la zone `.internal` | `roles/serveur_dns_public/`, `roles/serveur_powerdns/tasks/zones-publiques.yml` ; relations dérivées dans `scripts/site_inventaire.py` ; mots de flux `dns_public_site` / `primaires_dns_locataires` ; garde **P82** | `dns-interne.md` §Zones publiques |
| **Remise au client** | remettre un écosystème se fait en **deux temps** : l'*identité* le jour de la livraison (sa clé de voûte, sa voûte, sa racine d'AC), la *machine* à l'échéance (sa clé SSH entre, la nôtre sort, la voûte est re-clétée). Le registre déclare enfin le **responsable désigné** (D-18) | `scripts/remise.py` (`make remise-paquet`, `remise-inscrire`, `remise-recleer`) ; registre `remise.yml` chez le locataire ; garde **P80** | `remise-au-client.md` |
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
| SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du seed (`t17`, `t17serv`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 |
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
| Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 |
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51) Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10. Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents. sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous les chemins, donc un point de panne unique du plan de données — ce qui vidait aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés, bond3 répartis. D-51 ; D-05 renversée. Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui coupait l'accès d'administration au switch en cas d'erreur. D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau, où qu'elle vive », et le devis dérive s'il doit émettre une interface routée — uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux postures : le modèle public démontre celle où le switch route. Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige passerelle » et « routeur.ip == passerelle », une règle plus forte : une passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et elle a trouvé une sous-déclaration dans le modèle public. Quatre trous corrigés, tous de la même famille (une liste figée finit par mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le transit, switches d'accès sautant sa déclaration, et le switch de tête privé d'adresse de gestion par la suppression du SVI. D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1 revient à la passerelle. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
| Routage | **aucun commutateur ne route** : la frontière est le seul équipement L3 ; les switches commutent | `passerelle` dit qui porte la passerelle, le SVI se dérive du rôle du porteur | `decisions-architecture.md` D-49/50 |
| **Devis de service** | LIT le système en marche et le compare à ce que le plan dérive ; n'écrit rien (D-23/D-24 portés du réseau aux services). Le playbook **relève**, Python **compare** | `playbooks/maintenance/devis-*.yml` + `scripts/devis_*.py` ; cible `make <sujet>-plan` | `devis-services.md` |
| Accès d'administration | un **tunnel WireGuard nominatif** (instance `admins`) : un pair par personne et par appareil, le réseau du tunnel devient un réseau d'administration dont les pare-feux d'hôte, le contrat vers les locataires et les règles de bordure **dérivent**. Le runner n'est pas le rebond, et le document dit pourquoi | `scripts/vpn_admin.py` (`make vpn-admin-plan`) ; déclaré dans `acces_admin_vpn` (plan du site) | `acces-administration.md` |
frontière nord/sud : devis dérivé, lien de transit et les deux routes La bordure devient un artefact dérivé, comme le devis switch — et le chemin qui y mène est enfin déclaré. `make devis-opnsense` (+ preuve P24) dérive la politique de bordure du registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute volontairement parce qu'ils relèvent de la frontière et non du pare-feu d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur. Le lien manquait dans tous les fichiers : le devis switch ne contenait pas une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le déclare — il vit dans l'underlay et non dans un tenant parce que la frontière route vers TOUS les supernets tenants par le même saut, donc il ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes : l'aller (sortie générale) et le retour vers l'administration, dont l'absence a coûté la passe de déploiement du 2026-07-29 — la réponse revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger. La frontière est réglable depuis la console (section « Frontière » du panneau Intrants) ; les identifiants d'API restent interdits d'écriture par le GUI et vivent dans la voûte. Correctifs de la même passe : - le panneau refusait d'enregistrer les intrants de la frontière : le garde-fou confondait une référence de voûte `{{ vault_* }}` préservée avec un secret soumis. Il regarde désormais la valeur, pas le nom. - `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le playbook de clonage, voûte unique en dernier. - documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un reliquat de compatibilité. Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un par un ; résolution du jeton Proxmox vérifiée en exécution réelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
| Frontière nord/sud | les flux `pair: externe` — **sautés** par le pare-feu d'hôte — sont la politique de bordure | `scripts/devis_opnsense.py` (`make devis-opnsense`) ; garde d'accès admin = preuve **P24** | `frontiere-opnsense.md` |
> ⚠️ **Deux directions de binding, assumées** : `app→app` côté app (instancier),
> `app→base` côté base (registre, résolu en rôle pour que le secret ne quitte jamais le rôle).
> Ne pas unifier l'un dans l'autre sans raison. Cf. `bindings-conception.md`.
## Maturité et dette
- **Maturité des rôles, échafaudages, rôles-catégories inertes** : voir
`docs/projet/catalogue-services.md` « État d'implémentation » (source de vérité, tenue à jour).
docs : un README par rôle (12 manquants) + carte remise à l'état du code Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont désormais un README, au format maison (intention → rôle → variables → notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches : - serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à toute la flotte ; le vrai travail est dans le playbook de groupe. - hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de reconstruction (résolution par nom avant que PowerDNS existe) ; fichier entièrement géré, alias d'exposition best-effort. - resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le FQDN plutôt que le nom court (fédération + nom canonique pour verify-full). - serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise (static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine. - serveur_postfix : liens mailstore/milter, recopie de /etc/hosts dans le chroot. - serveur_rspamd : clé DKIM idempotente ; le domaine signé doit être PUBLIC en prod, sinon la signature ne vaut rien pour les MX distants. - client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client, deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ; hors-nœud n'est pas hors-site. - serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi, et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP. - serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO. - client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle avant de toucher /etc/resolv.conf). docs/carte-set-ops.md ne décrivait plus l'état du code : - expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») : expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs d'edge dérivés par instancier.py ; - 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de résolution, resoudre_base nommé dans les bindings app→base) ; - 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement, preuve/recette, exploitation courante, wiki — pans du corpus non indexés ; - ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix, requiert non consommé au déploiement (l'ordre vient des couches + du graphe). make verifier vert : ansible-lint 514 fichiers, tests, syntax-check, 23 preuves CONFORME / 0 échec / 0 sauté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:32:06 -04:00
- **Dette (audit 2026-07-03, revu le 2026-07-29)** :
- ✅ **soldé** — README de rôles : **tous les rôles en ont un** (les 12 manquants écrits le
2026-07-29 : `serveur_debian`, `hosts_statiques`, `resoudre_base`, `resoudre_annuaire`,
`serveur_dovecot`, `serveur_postfix`, `serveur_rspamd`, `client_backup`, `serveur_backup`,
`client_resolveur`, `serveur_oauth2_proxy`, `serveur_icingaweb2`).
docs : un README par rôle (12 manquants) + carte remise à l'état du code Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont désormais un README, au format maison (intention → rôle → variables → notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches : - serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à toute la flotte ; le vrai travail est dans le playbook de groupe. - hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de reconstruction (résolution par nom avant que PowerDNS existe) ; fichier entièrement géré, alias d'exposition best-effort. - resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le FQDN plutôt que le nom court (fédération + nom canonique pour verify-full). - serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise (static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine. - serveur_postfix : liens mailstore/milter, recopie de /etc/hosts dans le chroot. - serveur_rspamd : clé DKIM idempotente ; le domaine signé doit être PUBLIC en prod, sinon la signature ne vaut rien pour les MX distants. - client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client, deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ; hors-nœud n'est pas hors-site. - serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi, et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP. - serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO. - client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle avant de toucher /etc/resolv.conf). docs/carte-set-ops.md ne décrivait plus l'état du code : - expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») : expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs d'edge dérivés par instancier.py ; - 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de résolution, resoudre_base nommé dans les bindings app→base) ; - 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement, preuve/recette, exploitation courante, wiki — pans du corpus non indexés ; - ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix, requiert non consommé au déploiement (l'ordre vient des couches + du graphe). make verifier vert : ansible-lint 514 fichiers, tests, syntax-check, 23 preuves CONFORME / 0 échec / 0 sauté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:32:06 -04:00
- ✅ **soldé** — `expose` **est** consommé au déploiement : `plan/applications.yml` →
filtre `expositions_des_applications` → vhosts nginx dérivés
(`roles/serveur_nginx/tasks/main.yml`, template `expositions.conf.j2`, drapeau
`serveur_nginx_publier_expositions`), + alias `/etc/hosts` posés par `hosts_statiques`,
+ SANs des certificats d'edge dérivés par `scripts/instancier.py`.
- ⏳ **ouvert** — `meta/liens.yml` seulement sur `serveur_postfix` (`mailstore`, `milter`) :
les autres liens app→app viendront.
- ⏳ **ouvert** — `requiert` : câblé **côté GUI** (édition + validation
`valider_applications`) mais pas consommé au déploiement ; c'est un indice de dépendance
*applicative*. Les dépendances de **groupes** (celles qui pilotent l'ordre) vivent dans
`docs/dependances-groupes.yml` + `docs/couches-deploiement.yml`, et sont bel et bien
consommées par l'orchestrateur (`scripts/orchestrer.py` → `playbooks/site.yml`).
## Discipline
Avant de concevoir ou d'ajouter un mécanisme :
1. lire **cette page**, l'index (`docs/README.md`) et le document du sujet ;
2. **arpenter le code** (`grep`) et **lire les README des rôles** concernés ;
3. **éprouver l'outil AVANT d'écrire le rôle** qui l'enveloppe — une machine jetable hors
plan se fabrique en deux minutes (`make cloner-vm`, cf. `vm-lifecycle.md` §4bis). C'est
ce qui a évité les bugs de premier déploiement de rspamd, et ce qui a tranché le pivot
Stalwart → Postfix/Dovecot. Elle est invisible au moteur : **à détruire à la main** ;
4. **étendre / factoriser** l'existant plutôt qu'ajouter un chemin parallèle.
Un seul agent IA travaille dans le dépôt à la fois (Codex **ou** Claude) — cf. `AGENTS.md`.